From ea94bdd9ec656790852ce2f48f5a40d50cef25df Mon Sep 17 00:00:00 2001 From: Emanuel Posescu Date: Tue, 5 Jan 2021 16:46:09 +0200 Subject: [PATCH] Initial release --- .gitignore | 107 ++ LICENSE | 674 +++++++++ README.md | 132 ++ docs/SDK-Analysis.md | 203 +++ examples/addFR.js | 51 + examples/addICCard.js | 44 + examples/addPassCode.js | 38 + examples/clearFR.js | 38 + examples/clearICCards.js | 38 + examples/clearPassCodes.js | 38 + examples/clearPassageMode.js | 38 + examples/deletePassCode.js | 38 + examples/deletePassageMode.js | 44 + examples/getFR.js | 39 + examples/getICCards.js | 39 + examples/getPassCodes.js | 39 + examples/getPassageMode.js | 39 + examples/init.js | 49 + examples/listen.js | 34 + examples/lock.js | 45 + examples/reset.js | 45 + examples/setPassageMode.js | 44 + examples/status.js | 43 + examples/unlock.js | 45 + examples/updatePassCode.js | 38 + package.json | 66 + src/TTLockClient.ts | 119 ++ src/api/Command.ts | 35 + src/api/CommandEnvelope.ts | 263 ++++ src/api/Commands/AESKeyCommand.ts | 35 + src/api/Commands/AddAdminCommand.ts | 64 + src/api/Commands/AudioManageCommand.ts | 39 + src/api/Commands/AutoLockManageCommand.ts | 64 + src/api/Commands/CalibrationTimeCommand.ts | 23 + src/api/Commands/CheckAdminCommand.ts | 40 + src/api/Commands/CheckRandomCommand.ts | 27 + src/api/Commands/CheckUserTimeCommand.ts | 45 + src/api/Commands/ControlLampCommand.ts | 16 + .../Commands/ControlRemoteUnlockCommand.ts | 40 + src/api/Commands/CyclicDateCommand.ts | 104 ++ src/api/Commands/DeviceFeaturesCommand.ts | 90 ++ src/api/Commands/GetAdminCodeCommand.ts | 34 + .../Commands/GetKeyboardPasswordsCommand.ts | 98 ++ src/api/Commands/GetSwitchStateCommand.ts | 17 + src/api/Commands/InitCommand.ts | 17 + src/api/Commands/InitPasswordsCommand.ts | 75 + src/api/Commands/LockCommand.ts | 67 + src/api/Commands/ManageFRCommand.ts | 199 +++ src/api/Commands/ManageICCommand.ts | 208 +++ .../Commands/ManageKeyboardPasswordCommand.ts | 193 +++ src/api/Commands/OperateFinishedCommand.ts | 17 + src/api/Commands/PassageModeCommand.ts | 100 ++ src/api/Commands/ReadDeviceInfoCommand.ts | 29 + src/api/Commands/ResetLockCommand.ts | 16 + .../Commands/ScreenPasscodeManageCommand.ts | 44 + .../Commands/SearchBicycleStatusCommand.ts | 29 + .../Commands/SetAdminKeyboardPwdCommand.ts | 38 + src/api/Commands/UnknownCommand.ts | 16 + src/api/Commands/UnlockCommand.ts | 73 + src/api/Commands/index.ts | 32 + src/api/ValidityInfo.ts | 99 ++ src/api/commandBuilder.ts | 46 + src/constant/APICommand.ts | 235 +++ src/constant/ActionType.ts | 6 + src/constant/AudioManage.ts | 8 + src/constant/AutoLockOperate.ts | 12 + src/constant/CallbackOperationType.ts | 74 + src/constant/CommandResponse.ts | 37 + src/constant/CommandType.ts | 214 +++ src/constant/ConfigRemoteUnlock.ts | 8 + src/constant/ControlAction.ts | 17 + src/constant/CyclicOpType.ts | 15 + src/constant/DateConstant.ts | 6 + src/constant/DeviceInfoEnum.ts | 53 + src/constant/FeatureValue.ts | 160 +++ src/constant/ICOperate.ts | 20 + src/constant/KeyboardPwdType.ts | 23 + src/constant/Lock.ts | 141 ++ src/constant/LockedStatus.ts | 7 + src/constant/OperationType.ts | 8 + src/constant/PassageModeOperate.ts | 8 + src/constant/PassageModeType.ts | 6 + src/constant/PwdOperateType.ts | 28 + src/device/AdminType.ts | 6 + src/device/DeviceInfoType.ts | 47 + src/device/PrivateDataType.ts | 11 + src/device/TTBluetoothDevice.ts | 356 +++++ src/device/TTDevice.ts | 83 ++ src/device/TTLock.ts | 1115 +++++++++++++++ src/device/TTLockApi.ts | 1262 +++++++++++++++++ src/index.ts | 15 + src/scanner/BluetoothLeService.ts | 64 + src/scanner/DeviceInterface.ts | 69 + src/scanner/ScannerInterface.ts | 18 + src/scanner/noble/NobleCharacteristic.ts | 116 ++ src/scanner/noble/NobleDescriptor.ts | 85 ++ src/scanner/noble/NobleDevice.ts | 173 +++ src/scanner/noble/NobleScanner.ts | 108 ++ src/scanner/noble/NobleService.ts | 85 ++ src/store/TTLockData.ts | 26 + src/util/AESUtil.ts | 62 + src/util/CodecUtils.ts | 63 + src/util/digitUtil.ts | 9 + src/util/dscrc_table.ts | 20 + src/util/jsonUtil.ts | 22 + src/util/timeUtil.ts | 9 + src/util/timingUtil.ts | 11 + tools/debug.js | 108 ++ tsconfig.json | 71 + tslint.json | 11 + 110 files changed, 9610 insertions(+) create mode 100644 .gitignore create mode 100644 LICENSE create mode 100644 README.md create mode 100644 docs/SDK-Analysis.md create mode 100644 examples/addFR.js create mode 100644 examples/addICCard.js create mode 100644 examples/addPassCode.js create mode 100644 examples/clearFR.js create mode 100644 examples/clearICCards.js create mode 100644 examples/clearPassCodes.js create mode 100644 examples/clearPassageMode.js create mode 100644 examples/deletePassCode.js create mode 100644 examples/deletePassageMode.js create mode 100644 examples/getFR.js create mode 100644 examples/getICCards.js create mode 100644 examples/getPassCodes.js create mode 100644 examples/getPassageMode.js create mode 100644 examples/init.js create mode 100644 examples/listen.js create mode 100644 examples/lock.js create mode 100644 examples/reset.js create mode 100644 examples/setPassageMode.js create mode 100644 examples/status.js create mode 100644 examples/unlock.js create mode 100644 examples/updatePassCode.js create mode 100644 package.json create mode 100644 src/TTLockClient.ts create mode 100644 src/api/Command.ts create mode 100644 src/api/CommandEnvelope.ts create mode 100644 src/api/Commands/AESKeyCommand.ts create mode 100644 src/api/Commands/AddAdminCommand.ts create mode 100644 src/api/Commands/AudioManageCommand.ts create mode 100644 src/api/Commands/AutoLockManageCommand.ts create mode 100644 src/api/Commands/CalibrationTimeCommand.ts create mode 100644 src/api/Commands/CheckAdminCommand.ts create mode 100644 src/api/Commands/CheckRandomCommand.ts create mode 100644 src/api/Commands/CheckUserTimeCommand.ts create mode 100644 src/api/Commands/ControlLampCommand.ts create mode 100644 src/api/Commands/ControlRemoteUnlockCommand.ts create mode 100644 src/api/Commands/CyclicDateCommand.ts create mode 100644 src/api/Commands/DeviceFeaturesCommand.ts create mode 100644 src/api/Commands/GetAdminCodeCommand.ts create mode 100644 src/api/Commands/GetKeyboardPasswordsCommand.ts create mode 100644 src/api/Commands/GetSwitchStateCommand.ts create mode 100644 src/api/Commands/InitCommand.ts create mode 100644 src/api/Commands/InitPasswordsCommand.ts create mode 100644 src/api/Commands/LockCommand.ts create mode 100644 src/api/Commands/ManageFRCommand.ts create mode 100644 src/api/Commands/ManageICCommand.ts create mode 100644 src/api/Commands/ManageKeyboardPasswordCommand.ts create mode 100644 src/api/Commands/OperateFinishedCommand.ts create mode 100644 src/api/Commands/PassageModeCommand.ts create mode 100644 src/api/Commands/ReadDeviceInfoCommand.ts create mode 100644 src/api/Commands/ResetLockCommand.ts create mode 100644 src/api/Commands/ScreenPasscodeManageCommand.ts create mode 100644 src/api/Commands/SearchBicycleStatusCommand.ts create mode 100644 src/api/Commands/SetAdminKeyboardPwdCommand.ts create mode 100644 src/api/Commands/UnknownCommand.ts create mode 100644 src/api/Commands/UnlockCommand.ts create mode 100644 src/api/Commands/index.ts create mode 100644 src/api/ValidityInfo.ts create mode 100644 src/api/commandBuilder.ts create mode 100644 src/constant/APICommand.ts create mode 100644 src/constant/ActionType.ts create mode 100644 src/constant/AudioManage.ts create mode 100644 src/constant/AutoLockOperate.ts create mode 100644 src/constant/CallbackOperationType.ts create mode 100644 src/constant/CommandResponse.ts create mode 100644 src/constant/CommandType.ts create mode 100644 src/constant/ConfigRemoteUnlock.ts create mode 100644 src/constant/ControlAction.ts create mode 100644 src/constant/CyclicOpType.ts create mode 100644 src/constant/DateConstant.ts create mode 100644 src/constant/DeviceInfoEnum.ts create mode 100644 src/constant/FeatureValue.ts create mode 100644 src/constant/ICOperate.ts create mode 100644 src/constant/KeyboardPwdType.ts create mode 100644 src/constant/Lock.ts create mode 100644 src/constant/LockedStatus.ts create mode 100644 src/constant/OperationType.ts create mode 100644 src/constant/PassageModeOperate.ts create mode 100644 src/constant/PassageModeType.ts create mode 100644 src/constant/PwdOperateType.ts create mode 100644 src/device/AdminType.ts create mode 100644 src/device/DeviceInfoType.ts create mode 100644 src/device/PrivateDataType.ts create mode 100644 src/device/TTBluetoothDevice.ts create mode 100644 src/device/TTDevice.ts create mode 100644 src/device/TTLock.ts create mode 100644 src/device/TTLockApi.ts create mode 100644 src/index.ts create mode 100644 src/scanner/BluetoothLeService.ts create mode 100644 src/scanner/DeviceInterface.ts create mode 100644 src/scanner/ScannerInterface.ts create mode 100644 src/scanner/noble/NobleCharacteristic.ts create mode 100644 src/scanner/noble/NobleDescriptor.ts create mode 100644 src/scanner/noble/NobleDevice.ts create mode 100644 src/scanner/noble/NobleScanner.ts create mode 100644 src/scanner/noble/NobleService.ts create mode 100644 src/store/TTLockData.ts create mode 100644 src/util/AESUtil.ts create mode 100644 src/util/CodecUtils.ts create mode 100644 src/util/digitUtil.ts create mode 100644 src/util/dscrc_table.ts create mode 100644 src/util/jsonUtil.ts create mode 100644 src/util/timeUtil.ts create mode 100644 src/util/timingUtil.ts create mode 100644 tools/debug.js create mode 100644 tsconfig.json create mode 100644 tslint.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8ec09e5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,107 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# Next.js build output +.next + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and *not* Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +package-lock.json +lockData.json \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f288702 --- /dev/null +++ b/LICENSE @@ -0,0 +1,674 @@ + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 + + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md new file mode 100644 index 0000000..38b7a0f --- /dev/null +++ b/README.md @@ -0,0 +1,132 @@ +# ttlock-sdk-js + +The goal of this project is to make a partial JavaScript port of the TTLock Android SDK enough to make it work with the biometric locks. + +> This is just an SDK providing the means to communicate with the locks, it is not an app providing the full functionality of the TTLock app. If you are looking for an implementation please see [ttlock-hass-integration](https://github.com/kind3r/hass-addons) Home Assistant Addon. + +> Bluetooth implementation is using [@abandonware/noble](https://github.com/abandonware/noble) but other implementations are possible by extending [ScannerInterface](./src/scanner/ScannerInterface.ts) + +## Requirements +- node.js v12 or newer +- a bluetooth adapter on any platform* that [@abandonware/noble](https://github.com/abandonware/noble#installation) works on + +> *) It was tested on a Raspberry PI 3 running Debian and also under Home Assistant runing on an Intel NUC + +## Implemented features +- [X] discover locks +- [X] initialize (pair) locks +- [X] reset to factory defaults +- [X] lock +- [X] unlock +- [X] get lock/unlock status +- [X] set/get autolock time +- [X] add/edit/delete/clear passage mode +- [X] add/edit/remove keyboard passwords (PIN codes) +- [X] add/edit/remove fingerprints +- [X] add/edit/remove IC Cards + +## Planned development +- [ ] add some logger to separate debug events from normal ones +- [ ] proper timezone support +- [ ] cyclic based validity setup for credentials (ex.: Mo-Fr from 9AM to 5PM) +- [ ] get open/close logs +- [ ] API documentation +- [ ] receive lock/unlock events* + +> *) Receiving events is not possible at the moment as the Android SDK does not have this implemented (for obvious reasons, the phone is not always connected to the lock via BLE). It should be possible to analize the gateway and extract the commands required to activate the events. + +## **Known issues and limitations** +- Pairing the lock can sometimes fail. It is recommended to pair the lock before installing it on the door so you can use the button on the back to factory reset it. +- BLE signal is generaly bad, at least combined with the PI 3. Sometimes commands fail because of this (presumption). +- Editing validity intervals of fingerprints and IC Cards does not work. *Perhaps it is required to remove and re-add*. +- Some commands always have a bad CRC. +- The SDK only works with locks that use the V3 protocol for communication. + +## Sample usage of this SDK + +1. Clone the repo and install the dependencies `npm i`. +2. Check the installation prerequisites for your OS on the [@abandonware/noble](https://github.com/abandonware/noble#installation) GitHub page. Make sure you also read the [Running without root/sudo (Linux-specific)](https://github.com/abandonware/noble#running-without-rootsudo-linux-specific) section for running without sudo. + +The code for the followinng examples are located in the [examples](./examples) folder. + +### Initialisation + +`npm run init` - performs the initial pairing with the lock. + +The lock needs to be reset to factory defaults and it needs to be woke up by touching the keyboard. The lock stays alive for 10-15s and only in that interval it is discoverable so you need to time this right. + +> If the lock is woke up after the scan has started it won't be found. + +> If the lock is woke up too early, it can go back to sleep before the init process is completed. + +> The init script provides a countdown of 10 seconds, waking up the lock 5 seconds before the scan start proved to be most reliable. + +After the initialisation is completed, the script ouputs the credentials for the lock into the `lockData.json` file. This file is used by the other scripts. + +**Sometimes the pairing process fails** for reasons that are not quite clear. The pairing process has to be repeated until it succedes. Possible causes of failure are: +- the lock is too close to the PI +- something wrong in the BLE library used +- drivers + +In case the lock needs to be reseted to factory defaults, there is a switch on the back of the part that goes on the outside. Removing the metal cover will reveal this switch. Short pressing the switch will reboot the lock (one beep), long pressing for about 2-3 seconds will reset the lock to factory defaults (two beeps). + +### Lock/Unlock + +`npm run unlock` - unlock the lock +`npm run lock` - lock the lock + +Those 2 scripts read the lock credentials from `lockData.json` file generated by the init script, start searching for the lock and connect to it. Once the known lock is found and connected they perform the lock/unlock command. + +Bu default, auto locking is set for 5 seconds. So after unlocking, it will auto lock back. + +### Lock status + +`npm run status` - returns the lock or unlock status + +### Passage mode + +Passage mode disables autolock for the intervals you set. All unlock metods are now treated as toggle (lock/unlock) instead of just unlock and locking back after the autolock timeout. + +`npm run set-passage` - sets passage mode for friday all day +`npm run get-passage` - gets the passage mode intervals +`npm run delete-passage` - deletes the passage mode for friday all day +`npm run clear-passage` - deletes all passage mode intervals + +### Reset to factory defaults + +`npm run reset` - resets the lock to factory defaults + +Performs a soft reset of the lock to factory data. The credentials file `lockData.json` is automatically updated and the reseted lock is removed. + +### Passcodes management + +Passcodes or keyboard passcodes or pin codes allow oppening the lock using a 4-8 digits code. The passcodes can be permanent, one time, or limited time. + +`npm run add-passcode` - sets a permanent passcode **123456** available all the time +`npm run update-passcode` - updates the permanent passcode **123456** to **654321** +`npm run delete-passcode` - deletes the permanent passcode **654321** +`npm run clear-passcodes` - removes all passcodes + +### IC Card management + +IC cards are scanned and their serial number is returned. You can then add validity intervals for that card serial number. Also works with credit cards. + +`npm run add-card` - scans a card and adds a permanent validity +`npm run get-cards` - lists all the valid cards and their intervals +`npm run clear-cards` - removes all registered cards + +### Fingerprint management + +Fingerprints are scanned mutiple times during the add process. After scanning you can add validity intervals for that fingerprint. + +`npm run add-fingerprint` - scans a fingerprint and adds a permanent validity (it will timeout after 8.5 seconds if you do not scan a finger) +`npm run get-fingerprints` - lists all valid fingerprints and their intervals +`npm run clear-fingerprints` - removes all registered fingerprints + +## Credits + +- [Valentino Stillhardt (@Fusseldieb)](https://github.com/Fusseldieb) for initial protocol analysis and providing remote access to his lock + +## License + +[GPL-3.0](LICENSE) \ No newline at end of file diff --git a/docs/SDK-Analysis.md b/docs/SDK-Analysis.md new file mode 100644 index 0000000..c5c1fac --- /dev/null +++ b/docs/SDK-Analysis.md @@ -0,0 +1,203 @@ +# Original SDK analisys + +> The analisys is done only on the **`LockType.LOCK_TYPE_V3`** that use command protocol V3 lock as there are 7 types of locks that use slightly different type of protocol and features. It is possible to extend in the future to also support this type of locks. + +## SDK init and scanning + +- `TTLockClient::getDefault` - singleton*ish* pattern (*most of the classes are like this*) +- Calls `TTLockSDKApi::prepareBTService` + - Calls `BluetoothImpl::prepareBTService` + - register bluetooth device state change (**powering on**, **on**, **off**, etc.) callback `BroadcastReceiver::onReceive` + - this also starts the scan in case bluetooth turns on and scan was requested + - sets `mBluetoothManager = android.bluetooth.BluetoothManager`, `mBluetoothAdapter = mBluetoothManager.getAdapter()`; +```java +TTLockClient.getDefault().prepareBTService(getApplicationContext()); +``` + +- `TTLockClient::startScanLock(ScanLockCallback)` +- Calls `LockCallbackManager::setLockScanCallback(ScanLockCallback)` +- Calls `Calls TTLockSDKApi::startScan` + - Calls `BluetoothImpl::startScan` + - LockCallbackManager::getLockScanCallback + - `mScanner = ScannerCompat.getScanner()`, + - decides between `ScannerLollipop` and `ScannerImplJB` + - UUID_SERVICE = "**00001910-0000-1000-8000-00805f9b34fb**" + - `ScannerCompat::startScan(ScanCallback)`; + - `ScannerLollipop::startScanInternal` with uuids above + - **`android.bluetooth.le.BluetoothLeScanner.startScan`** + - **`ScanSettings.SCAN_MODE_LOW_LATENCY`** + - **`ScanFilter`** - uuids or null if scanAll + - `ScanCallbackImpl` + - `ScanCallbackImpl::onScanResult` + - `ScanCallback::onScan` with new `ExtendedBluetoothDevice(`**`android.bluetooth.le.ScanResult`**`)` + - **`android.bluetooth.le.ScanResult.getBytes`** Returns raw bytes of scan record. **This is what we need to determine all the parameters of the lock** + - `ScanLockCallback::onScanLockSuccess` +```java +TTLockClient.getDefault().startScanLock(new ScanLockCallback() { + @Override + public void onScanLockSuccess(ExtendedBluetoothDevice device) { + + } +}); +``` + +```java +TTLockClient.getDefault().stopBTService(); +``` + +## Initialize lock + +- `TTLockClient::initLock(ExtendedBluetoothDevice, InitLockCallback)` + - `ConnectManager::connect2Device`, `OperationType.INIT_LOCK` + - `BluetoothImpl::connect` + - **`android.bluetooth.BluetoothAdapter.getRemoteDevice.connectGatt`**, store as `BluetoothImpl::mBluetoothGatt`, used later in `sendComand` + - `BluetoothImpl::onConnectionStateChange` + - **`android.bluetooth.BluetoothGatt.discoverServices`** + - `BluetoothImpl::onServicesDiscovered` + - DEVICE_INFORMATION_SERVICE = "**0000180a-0000-1000-8000-00805f9b34fb**" + - list service characteristics + - READ_MODEL_NUMBER_UUID = "**00002a24-0000-1000-8000-00805f9b34fb**" + - READ_FIRMWARE_REVISION_UUID = "**00002a26-0000-1000-8000-00805f9b34fb**" + - READ_HARDWARE_REVISION_UUID = "**00002a27-0000-1000-8000-00805f9b34fb**" + - UUID_SERVICE = "**00001910-0000-1000-8000-00805f9b34fb**" + - list service characteristics + - UUID_WRITE = "**0000fff2-0000-1000-8000-00805f9b34fb**" => `BluetoothImpl::mNotifyCharacteristic` + - UUID_READ = "**0000fff4-0000-1000-8000-00805f9b34fb**", set notification for changes + - descriptor UUID_HEART_RATE_MEASUREMENT = "**00002902-0000-1000-8000-00805f9b34fb**", write **`android.bluetooth.BluetoothGattDescriptor.ENABLE_NOTIFICATION_VALUE`** + - `BluetoothImpl::onDescriptorWrite` + - triggers `ConnectManager::onConnectSuccess` + - `ConnectManager::onConnectSuccess` + - `TTLockSDKApi::initLock` + - `TransferData::setAPICommand(APICommand.OP_ADD_ADMIN)` + - `TransferData::setHotelData(device.getHotelData())` // not useful I think + - `CommandUtil::getAESKey(TransferData)`; + - `BluetoothImpl::aesKeyArray = CommandUtil::defaultAesKeyArray` + - `TransferData.setAesKeyArray(CommandUtil::defaultAesKeyArray)` + - `Command(LockVersion.lockVersion_V3)` + - `Command::setCommand(Command.COMM_GET_AES_KEY)` + - `Command::setData(Constant.VENDOR.getBytes(), CommandUtil::defaultAesKeyArray)` (Constant.VENDOR = "SCIENER") + - `TransferData::setTransferData(Command::buildCommand())` + - `BluetoothImpl::sendCommand(TransferData)` + - add 2 extra bytes at the end (CRLF) + - split data into a data queue `LinkedList` with at most 20 bytes length each + - **`android.bluetooth.BluetoothGatt::writeCharacteristic`**`(BluetoothImpl::mNotifyCharacteristic)` (UUID_WRITE = "**0000fff2-0000-1000-8000-00805f9b34fb**") + - `BluetoothImpl::onCharacteristicWrite` + - cycle **`android.bluetooth.BluetoothGatt::writeCharacteristic`** until no data is left + - delay of 2500ms - 5500ms (probably to wait for a characteristic change before disconnecting ?) + - `BluetoothImpl::disconnect` + - `BluetoothImpl::onCharacteristicChanged` (asssumption, because previous event is a dead end) + - store new data in `mReceivedDataBuffer` + - 2 stoppers for end of data: CRLF at the end or a calculated `leftRecDataCount` (we have to figure it out later) + - `BluetoothImpl::processCommandResponse` if end of data (remove CRLF if exists) + - huge function to process all possible command responses + - `new Command(values)`, check CRC + - `data = command.getData(aesKeyArray)` - for lock init, the CommandUtil::defaultAesKeyArray is used + - response is `Command.COMM_GET_AES_KEY` + - check data[1] == CommandResponse.SUCCESS + - `CommandUtil::V_addAdmin` with random `adminPs` and `unlockKey` (10 digit numbers, first digit always 0, so basically 9 digits) + - response is `Command.COMM_RESPONSE` + - command is `Command.COMM_ADD_ADMIN` + - check if data == `SCIENER` + - `CommandUtil.C_calibationTime` with time in ms and timezone. The response of this returns a bad CRC + - `BluetoothImpl::initLock` + - `CommandUtil.searchDeviceFeature` + - `BluetoothImpl::genCommandQue` Depending on the features, extra commands are ran agains the lock + - extra commands: + - Command.COMM_AUDIO_MANAGE -> CommandUtil_V3.audioManage + - Command.COMM_AUTO_LOCK_MANAGE -> CommandUtil.searchAutoLockTime + - Command.COMM_GET_ADMIN_CODE -> CommandUtil_V3.getAdminCode + - last command seems to set some random passwords `CommandUtil_V3::initPasswords`. After that `CommandUtil_V3::controlRemoteUnlock` and then `CommandUtil::operateFinished` + - a last check is being run `CommandUtil.readDeviceInfo` which starts another chain of commands to get more information about the device ending with finally calling `onInitLockSuccess` + - DeviceInfoType.MODEL_NUMBER -> modelNum + - DeviceInfoType.HARDWARE_REVISION -> hardwareRevision + - DeviceInfoType.FIRMWARE_REVISION -> firmwareRevision + - DeviceInfoType.MANUFACTURE_DATE -> factoryDate + - if FeatureValue.NB_LOCK : + - DeviceInfoType.NB_OPERATOR -> nbOperator + - DeviceInfoType.NB_IMEI -> nbNodeId + - DeviceInfoType.NB_CARD_INFO -> nbCardNumber + - DeviceInfoType.NB_RSSI -> nbRssi + - Posibilitity to run `CommandUtil_V3.configureNBServerAddress` with transferData.getPort() and transferData.getAddress() + - TODO: check what that is for + - else : + - DeviceInfoType.LOCK_CLOCK -> lockClock +```java +TTLockClient.getDefault().initLock(device, new InitLockCallback() { + @Override + public void onInitLockSuccess(String lockData,int specialValue) { + //init success + } + + @Override + public void onFail(LockError error) { + //failed + } +}); +``` + +## Reset lock + +- Same connection stuff as for init +- `TTLockSDKApi::resetLock` + - `CommandUtil::A_checkAdmin`:222 uid, lockVersionString, adminPs, unlockKey, lockFlagPos, aesKeyArray, 0, null, apiCommand (from lockData) + - `CommandUtil::A_checkAdmin`:263 (uid, lockVersionString, adminPs, unlockKey, lockFlagPos, aesKeyArray, 0, null, apiCommand) + - `CommandUtil_V3::checkAdmin` returns psFromLock, probably like a token + - `CommandUtil::checkRandom` + - `BluetoothImpl::isCheckedLockPermission = true` + - `CommandUtil.R_resetLock` + +```java +TTLockClient.getDefault().resetLock(mCurrentLock.getLockData(), mCurrentLock.getLockMac(),new ResetLockCallback() { + @Override + public void onResetLockSuccess() { + makeToast("-lock is reset and now upload to server -"); + uploadResetLock2Server(); + } + + @Override + public void onFail(LockError error) { + makeErrorToast(error); + } +}); +``` + +## Lock/unlock + +- Same connection stuff as for init +- `TTLockSDKApi::controlLock`, keyData.getUserType() == 110302 - does not seem to be used anywhere + - `TTLockSDKApi::unlockByUser` + - `CommandUtil::U_checkUserTime` + - `CommandUtil.G_unlock` + - `TTLockSDKApi::unlockByAdmin` + - `CommandUtil.A_checkAdmin` + - `CommandUtil.G_unlock` + - `TTLockSDKApi::lockByUser` + - `CommandUtil::U_checkUserTime` + - `CommandUtil.lock` + +```java +TTLockClient.getDefault().controlLock(ControlAction.UNLOCK, mMyTestLockEKey.getLockData(), mMyTestLockEKey.getLockMac(),new ControlLockCallback() { +    @Override +    public void onControlLockSuccess(int lockAction, int battery, int uniqueId) { +        Toast.makeText(UnlockActivity.this,"lock is unlock  success!",Toast.LENGTH_LONG).show(); +    } +  +    @Override +    public void onFail(LockError error) { +        Toast.makeText(UnlockActivity.this,"unLock fail!--" + error.getDescription(),Toast.LENGTH_LONG).show(); +    } +}); + +TTLockClient.getDefault().controlLock(ControlAction.LOCK, mMyTestLockEKey.getLockData(), mMyTestLockEKey.getLockMac(),new ControlLockCallback() { + @Override + public void onControlLockSuccess(int lockAction, int battery, int uniqueId) { + Toast.makeText(UnlockActivity.this,"lock is locked!",Toast.LENGTH_LONG).show(); + } + + @Override + public void onFail(LockError error) { + Toast.makeText(UnlockActivity.this,"lock lock fail!--" + error.getDescription(),Toast.LENGTH_LONG).show(); + } +}); +``` + diff --git a/examples/addFR.js b/examples/addFR.js new file mode 100644 index 0000000..f68a7f7 --- /dev/null +++ b/examples/addFR.js @@ -0,0 +1,51 @@ +'use strict'; + +const { TTLockClient, sleep, PassageModeType } = require('../dist'); +const fs = require('fs/promises'); +const settingsFile = "lockData.json"; + +async function doStuff() { + let lockData; + try { + await fs.access(settingsFile); + const lockDataTxt = (await fs.readFile(settingsFile)).toString(); + lockData = JSON.parse(lockDataTxt); + } catch (error) {} + + const client = new TTLockClient({ + lockData: lockData + }); + await client.prepareBTService(); + client.startScanLock(); + console.log("Scan started"); + client.on("foundLock", async (lock) => { + await lock.connect(true); + console.log(lock.toJSON()); + console.log(); + + if (lock.isInitialized() && lock.isPaired()) { + console.log("Trying to add Fingerprint"); + console.log(); + console.log(); + let progress = 0; + lock.on("scanFRStart", () => { + console.log(); + console.log("Ready to scan a finger"); + console.log(); + }); + lock.on("scanFRProgress", () => { + progress++; + console.log(); + console.log("Scanning progress", progress); + console.log(); + }); + const result = await lock.addFingerprint('202001010000', '202212312359'); + console.log(result); + await lock.disconnect(); + + process.exit(0); + } + }); +} + +doStuff(); \ No newline at end of file diff --git a/examples/addICCard.js b/examples/addICCard.js new file mode 100644 index 0000000..4db52be --- /dev/null +++ b/examples/addICCard.js @@ -0,0 +1,44 @@ +'use strict'; + +const { TTLockClient, sleep, PassageModeType } = require('../dist'); +const fs = require('fs/promises'); +const settingsFile = "lockData.json"; + +async function doStuff() { + let lockData; + try { + await fs.access(settingsFile); + const lockDataTxt = (await fs.readFile(settingsFile)).toString(); + lockData = JSON.parse(lockDataTxt); + } catch (error) {} + + const client = new TTLockClient({ + lockData: lockData + }); + await client.prepareBTService(); + client.startScanLock(); + console.log("Scan started"); + client.on("foundLock", async (lock) => { + await lock.connect(true); + console.log(lock.toJSON()); + console.log(); + + if (lock.isInitialized() && lock.isPaired()) { + console.log("Trying to add IC Card"); + console.log(); + console.log(); + lock.on("scanICStart", () => { + console.log(); + console.log("Ready to scan an IC Card"); + console.log(); + }); + const result = await lock.addICCard('202001010000', '202212312359'); + console.log(result); + await lock.disconnect(); + + process.exit(0); + } + }); +} + +doStuff(); \ No newline at end of file diff --git a/examples/addPassCode.js b/examples/addPassCode.js new file mode 100644 index 0000000..8dd14b9 --- /dev/null +++ b/examples/addPassCode.js @@ -0,0 +1,38 @@ +'use strict'; + +const { TTLockClient, sleep, PassageModeType } = require('../dist'); +const fs = require('fs/promises'); +const settingsFile = "lockData.json"; + +async function doStuff() { + let lockData; + try { + await fs.access(settingsFile); + const lockDataTxt = (await fs.readFile(settingsFile)).toString(); + lockData = JSON.parse(lockDataTxt); + } catch (error) {} + + const client = new TTLockClient({ + lockData: lockData + }); + await client.prepareBTService(); + client.startScanLock(); + console.log("Scan started"); + client.on("foundLock", async (lock) => { + await lock.connect(true); + console.log(lock.toJSON()); + console.log(); + + if (lock.isInitialized() && lock.isPaired()) { + console.log("Trying to add passcode"); + console.log(); + console.log(); + const result = await lock.addPassCode(1, '123456', '202001010000', '202212312359'); + await lock.disconnect(); + + process.exit(0); + } + }); +} + +doStuff(); \ No newline at end of file diff --git a/examples/clearFR.js b/examples/clearFR.js new file mode 100644 index 0000000..c5ca802 --- /dev/null +++ b/examples/clearFR.js @@ -0,0 +1,38 @@ +'use strict'; + +const { TTLockClient, sleep, PassageModeType } = require('../dist'); +const fs = require('fs/promises'); +const settingsFile = "lockData.json"; + +async function doStuff() { + let lockData; + try { + await fs.access(settingsFile); + const lockDataTxt = (await fs.readFile(settingsFile)).toString(); + lockData = JSON.parse(lockDataTxt); + } catch (error) {} + + const client = new TTLockClient({ + lockData: lockData + }); + await client.prepareBTService(); + client.startScanLock(); + console.log("Scan started"); + client.on("foundLock", async (lock) => { + await lock.connect(true); + console.log(lock.toJSON()); + console.log(); + + if (lock.isInitialized() && lock.isPaired()) { + console.log("Trying to clear fingerprints"); + console.log(); + console.log(); + const result = await lock.clearFingerprints(); + await lock.disconnect(); + + process.exit(0); + } + }); +} + +doStuff(); \ No newline at end of file diff --git a/examples/clearICCards.js b/examples/clearICCards.js new file mode 100644 index 0000000..4446272 --- /dev/null +++ b/examples/clearICCards.js @@ -0,0 +1,38 @@ +'use strict'; + +const { TTLockClient, sleep, PassageModeType } = require('../dist'); +const fs = require('fs/promises'); +const settingsFile = "lockData.json"; + +async function doStuff() { + let lockData; + try { + await fs.access(settingsFile); + const lockDataTxt = (await fs.readFile(settingsFile)).toString(); + lockData = JSON.parse(lockDataTxt); + } catch (error) {} + + const client = new TTLockClient({ + lockData: lockData + }); + await client.prepareBTService(); + client.startScanLock(); + console.log("Scan started"); + client.on("foundLock", async (lock) => { + await lock.connect(true); + console.log(lock.toJSON()); + console.log(); + + if (lock.isInitialized() && lock.isPaired()) { + console.log("Trying to clear IC cards"); + console.log(); + console.log(); + const result = await lock.clearICCards(); + await lock.disconnect(); + + process.exit(0); + } + }); +} + +doStuff(); \ No newline at end of file diff --git a/examples/clearPassCodes.js b/examples/clearPassCodes.js new file mode 100644 index 0000000..cb86daf --- /dev/null +++ b/examples/clearPassCodes.js @@ -0,0 +1,38 @@ +'use strict'; + +const { TTLockClient, sleep, PassageModeType } = require('../dist'); +const fs = require('fs/promises'); +const settingsFile = "lockData.json"; + +async function doStuff() { + let lockData; + try { + await fs.access(settingsFile); + const lockDataTxt = (await fs.readFile(settingsFile)).toString(); + lockData = JSON.parse(lockDataTxt); + } catch (error) {} + + const client = new TTLockClient({ + lockData: lockData + }); + await client.prepareBTService(); + client.startScanLock(); + console.log("Scan started"); + client.on("foundLock", async (lock) => { + await lock.connect(true); + console.log(lock.toJSON()); + console.log(); + + if (lock.isInitialized() && lock.isPaired()) { + console.log("Trying to clear passcodes"); + console.log(); + console.log(); + const result = await lock.clearPassCodes(); + await lock.disconnect(); + + process.exit(0); + } + }); +} + +doStuff(); \ No newline at end of file diff --git a/examples/clearPassageMode.js b/examples/clearPassageMode.js new file mode 100644 index 0000000..511ec00 --- /dev/null +++ b/examples/clearPassageMode.js @@ -0,0 +1,38 @@ +'use strict'; + +const { TTLockClient, sleep, PassageModeType } = require('../dist'); +const fs = require('fs/promises'); +const settingsFile = "lockData.json"; + +async function doStuff() { + let lockData; + try { + await fs.access(settingsFile); + const lockDataTxt = (await fs.readFile(settingsFile)).toString(); + lockData = JSON.parse(lockDataTxt); + } catch (error) {} + + const client = new TTLockClient({ + lockData: lockData + }); + await client.prepareBTService(); + client.startScanLock(); + console.log("Scan started"); + client.on("foundLock", async (lock) => { + await lock.connect(true); + console.log(lock.toJSON()); + console.log(); + + if (lock.isInitialized() && lock.isPaired()) { + console.log("Trying to clear passage mode"); + console.log(); + console.log(); + const result = await lock.clearPassageMode(); + await lock.disconnect(); + + process.exit(0); + } + }); +} + +doStuff(); \ No newline at end of file diff --git a/examples/deletePassCode.js b/examples/deletePassCode.js new file mode 100644 index 0000000..840a7c1 --- /dev/null +++ b/examples/deletePassCode.js @@ -0,0 +1,38 @@ +'use strict'; + +const { TTLockClient, sleep, PassageModeType } = require('../dist'); +const fs = require('fs/promises'); +const settingsFile = "lockData.json"; + +async function doStuff() { + let lockData; + try { + await fs.access(settingsFile); + const lockDataTxt = (await fs.readFile(settingsFile)).toString(); + lockData = JSON.parse(lockDataTxt); + } catch (error) {} + + const client = new TTLockClient({ + lockData: lockData + }); + await client.prepareBTService(); + client.startScanLock(); + console.log("Scan started"); + client.on("foundLock", async (lock) => { + await lock.connect(true); + console.log(lock.toJSON()); + console.log(); + + if (lock.isInitialized() && lock.isPaired()) { + console.log("Trying to delete passcode"); + console.log(); + console.log(); + const result = await lock.deletePassCode(1, '654321'); + await lock.disconnect(); + + process.exit(0); + } + }); +} + +doStuff(); diff --git a/examples/deletePassageMode.js b/examples/deletePassageMode.js new file mode 100644 index 0000000..02214e0 --- /dev/null +++ b/examples/deletePassageMode.js @@ -0,0 +1,44 @@ +'use strict'; + +const { TTLockClient, sleep, PassageModeType } = require('../dist'); +const fs = require('fs/promises'); +const settingsFile = "lockData.json"; + +async function doStuff() { + let lockData; + try { + await fs.access(settingsFile); + const lockDataTxt = (await fs.readFile(settingsFile)).toString(); + lockData = JSON.parse(lockDataTxt); + } catch (error) {} + + const client = new TTLockClient({ + lockData: lockData + }); + await client.prepareBTService(); + client.startScanLock(); + console.log("Scan started"); + client.on("foundLock", async (lock) => { + await lock.connect(true); + console.log(lock.toJSON()); + console.log(); + + if (lock.isInitialized() && lock.isPaired()) { + console.log("Trying to set passage mode"); + console.log(); + console.log(); + const result = await lock.deletePassageMode({ + type: PassageModeType.WEEKLY, + weekOrDay: 5, + month: 0, + startHour: "0000", + endHour: "2359" + }); + await lock.disconnect(); + + process.exit(0); + } + }); +} + +doStuff(); \ No newline at end of file diff --git a/examples/getFR.js b/examples/getFR.js new file mode 100644 index 0000000..8dc6a8f --- /dev/null +++ b/examples/getFR.js @@ -0,0 +1,39 @@ +'use strict'; + +const { TTLockClient, sleep, PassageModeType } = require('../dist'); +const fs = require('fs/promises'); +const settingsFile = "lockData.json"; + +async function doStuff() { + let lockData; + try { + await fs.access(settingsFile); + const lockDataTxt = (await fs.readFile(settingsFile)).toString(); + lockData = JSON.parse(lockDataTxt); + } catch (error) {} + + const client = new TTLockClient({ + lockData: lockData + }); + await client.prepareBTService(); + client.startScanLock(); + console.log("Scan started"); + client.on("foundLock", async (lock) => { + await lock.connect(true); + console.log(lock.toJSON()); + console.log(); + + if (lock.isInitialized() && lock.isPaired()) { + console.log("Trying to get fingerprints"); + console.log(); + console.log(); + const result = await lock.getFingerprints(); + await lock.disconnect(); + console.log(result); + + process.exit(0); + } + }); +} + +doStuff(); \ No newline at end of file diff --git a/examples/getICCards.js b/examples/getICCards.js new file mode 100644 index 0000000..29d0c96 --- /dev/null +++ b/examples/getICCards.js @@ -0,0 +1,39 @@ +'use strict'; + +const { TTLockClient, sleep, PassageModeType } = require('../dist'); +const fs = require('fs/promises'); +const settingsFile = "lockData.json"; + +async function doStuff() { + let lockData; + try { + await fs.access(settingsFile); + const lockDataTxt = (await fs.readFile(settingsFile)).toString(); + lockData = JSON.parse(lockDataTxt); + } catch (error) {} + + const client = new TTLockClient({ + lockData: lockData + }); + await client.prepareBTService(); + client.startScanLock(); + console.log("Scan started"); + client.on("foundLock", async (lock) => { + await lock.connect(true); + console.log(lock.toJSON()); + console.log(); + + if (lock.isInitialized() && lock.isPaired()) { + console.log("Trying to get IC Cards"); + console.log(); + console.log(); + const result = await lock.getICCards(); + await lock.disconnect(); + console.log(result); + + process.exit(0); + } + }); +} + +doStuff(); \ No newline at end of file diff --git a/examples/getPassCodes.js b/examples/getPassCodes.js new file mode 100644 index 0000000..4e66cef --- /dev/null +++ b/examples/getPassCodes.js @@ -0,0 +1,39 @@ +'use strict'; + +const { TTLockClient, sleep, PassageModeType } = require('../dist'); +const fs = require('fs/promises'); +const settingsFile = "lockData.json"; + +async function doStuff() { + let lockData; + try { + await fs.access(settingsFile); + const lockDataTxt = (await fs.readFile(settingsFile)).toString(); + lockData = JSON.parse(lockDataTxt); + } catch (error) {} + + const client = new TTLockClient({ + lockData: lockData + }); + await client.prepareBTService(); + client.startScanLock(); + console.log("Scan started"); + client.on("foundLock", async (lock) => { + await lock.connect(true); + console.log(lock.toJSON()); + console.log(); + + if (lock.isInitialized() && lock.isPaired()) { + console.log("Trying to get passcodes"); + console.log(); + console.log(); + const result = await lock.getPassCodes(); + await lock.disconnect(); + console.log(result); + + process.exit(0); + } + }); +} + +doStuff(); \ No newline at end of file diff --git a/examples/getPassageMode.js b/examples/getPassageMode.js new file mode 100644 index 0000000..c0a45fc --- /dev/null +++ b/examples/getPassageMode.js @@ -0,0 +1,39 @@ +'use strict'; + +const { TTLockClient, sleep, PassageModeType } = require('../dist'); +const fs = require('fs/promises'); +const settingsFile = "lockData.json"; + +async function doStuff() { + let lockData; + try { + await fs.access(settingsFile); + const lockDataTxt = (await fs.readFile(settingsFile)).toString(); + lockData = JSON.parse(lockDataTxt); + } catch (error) {} + + const client = new TTLockClient({ + lockData: lockData + }); + await client.prepareBTService(); + client.startScanLock(); + console.log("Scan started"); + client.on("foundLock", async (lock) => { + await lock.connect(true); + console.log(lock.toJSON()); + console.log(); + + if (lock.isInitialized() && lock.isPaired()) { + console.log("Trying to set passage mode"); + console.log(); + console.log(); + const result = await lock.getPassageMode(); + console.log(result); + await lock.disconnect(); + + process.exit(0); + } + }); +} + +doStuff(); \ No newline at end of file diff --git a/examples/init.js b/examples/init.js new file mode 100644 index 0000000..52b1c84 --- /dev/null +++ b/examples/init.js @@ -0,0 +1,49 @@ +'use strict'; + +const { TTLockClient, sleep } = require('../dist'); +const fs = require('fs/promises'); +const settingsFile = "lockData.json"; + +async function doStuff() { + let lockData; + try { + await fs.access(settingsFile); + const lockDataTxt = (await fs.readFile(settingsFile)).toString(); + lockData = JSON.parse(lockDataTxt); + } catch (error) {} + + const client = new TTLockClient({ + lockData: lockData + }); + await client.prepareBTService(); + for (let i = 10; i > 0; i--) { + console.log("Starting scan...", i); + await sleep(1000); + } + client.startScanLock(); + console.log("Scan started"); + client.on("foundLock", async (lock) => { + await lock.connect(); + console.log(lock.toJSON()); + console.log(); + + if (!lock.isInitialized()) { + console.log("Trying to init the lock"); + console.log(); + console.log(); + const inited = await lock.initLock(); + await lock.disconnect(); + const newLockData = client.getLockData(); + console.log(JSON.stringify(newLockData)); + try { + await fs.writeFile(settingsFile, Buffer.from(JSON.stringify(newLockData))); + } catch (error) { + process.exit(1); + } + + process.exit(0); + } + }); +} + +doStuff(); \ No newline at end of file diff --git a/examples/listen.js b/examples/listen.js new file mode 100644 index 0000000..2013c0c --- /dev/null +++ b/examples/listen.js @@ -0,0 +1,34 @@ +'use strict'; + +const { TTLockClient, sleep } = require('../dist'); +const fs = require('fs/promises'); +const settingsFile = "lockData.json"; + +async function doStuff() { + let lockData; + try { + await fs.access(settingsFile); + const lockDataTxt = (await fs.readFile(settingsFile)).toString(); + lockData = JSON.parse(lockDataTxt); + } catch (error) {} + + const client = new TTLockClient({ + lockData: lockData + }); + await client.prepareBTService(); + client.startScanLock(); + console.log("Scan started"); + client.on("foundLock", async (lock) => { + await lock.connect(); + console.log(lock.toJSON()); + console.log(); + + if (lock.isInitialized() && lock.isPaired()) { + console.log("Connected to a known lock"); + console.log(); + console.log(); + } + }); +} + +doStuff(); \ No newline at end of file diff --git a/examples/lock.js b/examples/lock.js new file mode 100644 index 0000000..07f101e --- /dev/null +++ b/examples/lock.js @@ -0,0 +1,45 @@ +'use strict'; + +const { TTLockClient, sleep } = require('../dist'); +const fs = require('fs/promises'); +const settingsFile = "lockData.json"; + +async function doStuff() { + let lockData; + try { + await fs.access(settingsFile); + const lockDataTxt = (await fs.readFile(settingsFile)).toString(); + lockData = JSON.parse(lockDataTxt); + } catch (error) {} + + const client = new TTLockClient({ + lockData: lockData + }); + await client.prepareBTService(); + client.startScanLock(); + console.log("Scan started"); + client.on("foundLock", async (lock) => { + await lock.connect(true); + console.log(lock.toJSON()); + console.log(); + + if (lock.isInitialized() && lock.isPaired()) { + console.log("Trying to lock the lock"); + console.log(); + console.log(); + const unlock = await lock.lock(); + await lock.disconnect(); + const newLockData = client.getLockData(); + console.log(JSON.stringify(newLockData)); + try { + await fs.writeFile(settingsFile, Buffer.from(JSON.stringify(newLockData))); + } catch (error) { + process.exit(1); + } + + process.exit(0); + } + }); +} + +doStuff(); \ No newline at end of file diff --git a/examples/reset.js b/examples/reset.js new file mode 100644 index 0000000..603fa38 --- /dev/null +++ b/examples/reset.js @@ -0,0 +1,45 @@ +'use strict'; + +const { TTLockClient, sleep } = require('../dist'); +const fs = require('fs/promises'); +const settingsFile = "lockData.json"; + +async function doStuff() { + let lockData; + try { + await fs.access(settingsFile); + const lockDataTxt = (await fs.readFile(settingsFile)).toString(); + lockData = JSON.parse(lockDataTxt); + } catch (error) {} + + const client = new TTLockClient({ + lockData: lockData + }); + await client.prepareBTService(); + client.startScanLock(); + console.log("Scan started"); + client.on("foundLock", async (lock) => { + await lock.connect(true); + console.log(lock.toJSON()); + console.log(); + + if (lock.isInitialized() && lock.isPaired()) { + console.log("Trying to reset the lock"); + console.log(); + console.log(); + const reset = await lock.resetLock(); + await lock.disconnect(); + const newLockData = client.getLockData(); + console.log(JSON.stringify(newLockData)); + try { + await fs.writeFile(settingsFile, Buffer.from(JSON.stringify(newLockData))); + } catch (error) { + process.exit(1); + } + + process.exit(0); + } + }); +} + +doStuff(); \ No newline at end of file diff --git a/examples/setPassageMode.js b/examples/setPassageMode.js new file mode 100644 index 0000000..126cb1d --- /dev/null +++ b/examples/setPassageMode.js @@ -0,0 +1,44 @@ +'use strict'; + +const { TTLockClient, sleep, PassageModeType } = require('../dist'); +const fs = require('fs/promises'); +const settingsFile = "lockData.json"; + +async function doStuff() { + let lockData; + try { + await fs.access(settingsFile); + const lockDataTxt = (await fs.readFile(settingsFile)).toString(); + lockData = JSON.parse(lockDataTxt); + } catch (error) {} + + const client = new TTLockClient({ + lockData: lockData + }); + await client.prepareBTService(); + client.startScanLock(); + console.log("Scan started"); + client.on("foundLock", async (lock) => { + await lock.connect(true); + console.log(lock.toJSON()); + console.log(); + + if (lock.isInitialized() && lock.isPaired()) { + console.log("Trying to set passage mode"); + console.log(); + console.log(); + const result = await lock.setPassageMode({ + type: PassageModeType.WEEKLY, + weekOrDay: 5, + month: 0, + startHour: "0000", + endHour: "2359" + }); + await lock.disconnect(); + + process.exit(0); + } + }); +} + +doStuff(); \ No newline at end of file diff --git a/examples/status.js b/examples/status.js new file mode 100644 index 0000000..fea9ade --- /dev/null +++ b/examples/status.js @@ -0,0 +1,43 @@ +'use strict'; + +const { TTLockClient, sleep, PassageModeType } = require('../dist'); +const fs = require('fs/promises'); +const settingsFile = "lockData.json"; + +async function doStuff() { + let lockData; + try { + await fs.access(settingsFile); + const lockDataTxt = (await fs.readFile(settingsFile)).toString(); + lockData = JSON.parse(lockDataTxt); + } catch (error) {} + + const client = new TTLockClient({ + lockData: lockData + }); + await client.prepareBTService(); + client.startScanLock(); + console.log("Scan started"); + client.on("foundLock", async (lock) => { + await lock.connect(true); + console.log(lock.toJSON()); + console.log(); + + if (lock.isInitialized() && lock.isPaired()) { + console.log("Trying to get lock/unlock status"); + console.log(); + console.log(); + const result = await lock.getLockStatus(); + if (result != -1) { + console.log("Lock is locked", result); + } else { + console.log("Failed to get lock status"); + } + await lock.disconnect(); + + process.exit(0); + } + }); +} + +doStuff(); \ No newline at end of file diff --git a/examples/unlock.js b/examples/unlock.js new file mode 100644 index 0000000..58c0e27 --- /dev/null +++ b/examples/unlock.js @@ -0,0 +1,45 @@ +'use strict'; + +const { TTLockClient, sleep } = require('../dist'); +const fs = require('fs/promises'); +const settingsFile = "lockData.json"; + +async function doStuff() { + let lockData; + try { + await fs.access(settingsFile); + const lockDataTxt = (await fs.readFile(settingsFile)).toString(); + lockData = JSON.parse(lockDataTxt); + } catch (error) {} + + const client = new TTLockClient({ + lockData: lockData + }); + await client.prepareBTService(); + client.startScanLock(); + console.log("Scan started"); + client.on("foundLock", async (lock) => { + await lock.connect(true); + console.log(lock.toJSON()); + console.log(); + + if (lock.isInitialized() && lock.isPaired()) { + console.log("Trying to unlock the lock"); + console.log(); + console.log(); + const unlock = await lock.unlock(); + await lock.disconnect(); + const newLockData = client.getLockData(); + console.log(JSON.stringify(newLockData)); + try { + await fs.writeFile(settingsFile, Buffer.from(JSON.stringify(newLockData))); + } catch (error) { + process.exit(1); + } + + process.exit(0); + } + }); +} + +doStuff(); \ No newline at end of file diff --git a/examples/updatePassCode.js b/examples/updatePassCode.js new file mode 100644 index 0000000..9102f79 --- /dev/null +++ b/examples/updatePassCode.js @@ -0,0 +1,38 @@ +'use strict'; + +const { TTLockClient, sleep, PassageModeType } = require('../dist'); +const fs = require('fs/promises'); +const settingsFile = "lockData.json"; + +async function doStuff() { + let lockData; + try { + await fs.access(settingsFile); + const lockDataTxt = (await fs.readFile(settingsFile)).toString(); + lockData = JSON.parse(lockDataTxt); + } catch (error) {} + + const client = new TTLockClient({ + lockData: lockData + }); + await client.prepareBTService(); + client.startScanLock(); + console.log("Scan started"); + client.on("foundLock", async (lock) => { + await lock.connect(true); + console.log(lock.toJSON()); + console.log(); + + if (lock.isInitialized() && lock.isPaired()) { + console.log("Trying to edit passcode"); + console.log(); + console.log(); + const result = await lock.updatePassCode(1, '123456', '654321', '202001010000', '202212312359'); + await lock.disconnect(); + + process.exit(0); + } + }); +} + +doStuff(); \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..c977d95 --- /dev/null +++ b/package.json @@ -0,0 +1,66 @@ +{ + "name": "ttlock-sdk-js", + "version": "0.1.0", + "description": "JavaScript port of the TTLock Android SDK", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist/**/*" + ], + "scripts": { + "build": "rm -rf ./dist && tsc", + "prepare": "npm run build", + "init": "npm run build && node ./examples/init.js", + "reset": "npm run build && node ./examples/reset.js", + "unlock": "npm run build && node ./examples/unlock.js", + "lock": "npm run build && node ./examples/lock.js", + "status": "npm run build && node ./examples/status.js", + "set-passage": "npm run build && node ./examples/setPassageMode.js", + "get-passage": "npm run build && node ./examples/getPassageMode.js", + "delete-passage": "npm run build && node ./examples/deletePassageMode.js", + "clear-passage": "npm run build && node ./examples/clearPassageMode.js", + "add-passcode": "npm run build && node ./examples/addPassCode.js", + "update-passcode": "npm run build && node ./examples/updatePassCode.js", + "delete-passcode": "npm run build && node ./examples/deletePassCode.js", + "clear-passcodes": "npm run build && node ./examples/clearPassCodes.js", + "get-passcodes": "npm run build && node ./examples/getPassCodes.js", + "add-card": "npm run build && node ./examples/addICCard.js", + "get-cards": "npm run build && node ./examples/getICCards.js", + "clear-cards": "npm run build && node ./examples/clearICCards.js", + "add-fingerprint": "npm run build && node ./examples/addFR.js", + "get-fingerprints": "npm run build && node ./examples/getFR.js", + "clear-fingerprints": "npm run build && node ./examples/clearFR.js", + "listen": "npm run build && node ./examples/listen.js", + "debug-tool": "npm run build && NOBLE_REPORT_ALL_HCI_EVENTS=1 node ./tools/dedbug.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/kind3r/ttlock-sdk-js.git" + }, + "keywords": [ + "ttlock", + "sdk", + "library", + "javascript", + "smartlock", + "smart-lock", + "iot", + "unofficial" + ], + "author": "Emanuel Posescu ", + "license": "GPL-3.0", + "bugs": { + "url": "https://github.com/kind3r/ttlock-sdk-js/issues" + }, + "homepage": "https://github.com/kind3r/ttlock-sdk-js#readme", + "devDependencies": { + "@tsconfig/node12": "^1.0.7", + "@types/node": "^14.14.8", + "tslint": "^6.1.3", + "typescript": "^4.0.5" + }, + "dependencies": { + "@abandonware/noble": "^1.9.2-10", + "moment": "^2.29.1" + } +} diff --git a/src/TTLockClient.ts b/src/TTLockClient.ts new file mode 100644 index 0000000..01918be --- /dev/null +++ b/src/TTLockClient.ts @@ -0,0 +1,119 @@ +'use strict'; + +import events from "events"; +import { LockType } from "./constant/Lock"; +import { TTBluetoothDevice } from "./device/TTBluetoothDevice"; +import { TTLock } from "./device/TTLock"; + +import { BluetoothLeService, TTLockUUIDs, ScannerType } from "./scanner/BluetoothLeService"; +import { TTLockData } from "./store/TTLockData"; +import { sleep } from "./util/timingUtil"; + +export interface Settings { + uuids?: string[]; + scannerType?: ScannerType; + lockData?: TTLockData[]; +} + +export interface TTLockClient { + on(event: "foundLock", listener: (lock: TTLock) => void): this; + on(event: "scanStart", listener: () => void): this; + on(event: "scanStop", listener: () => void): this; +} + +export class TTLockClient extends events.EventEmitter implements TTLockClient { + bleService: BluetoothLeService | null = null; + uuids: string[]; + scannerType: ScannerType = "noble"; + lockData: Map; + private adapterReady: boolean; + + constructor(options: Settings) { + super(); + + this.adapterReady = false; + + if (options.uuids) { + this.uuids = options.uuids; + } else { + this.uuids = TTLockUUIDs; + } + + if (options.scannerType) { + this.scannerType = options.scannerType; + } + + this.lockData = new Map(); + if (options.lockData && options.lockData.length > 0) { + options.lockData.forEach((lockData) => { + this.lockData.set(lockData.address, lockData); + }); + } + } + + async prepareBTService(): Promise { + if (this.bleService == null) { + this.bleService = new BluetoothLeService(this.uuids, this.scannerType); + this.bleService.on("ready", () => this.adapterReady = true); + this.bleService.on("discover", this.onScanResult.bind(this)); + this.bleService.on("scanStart", () => this.emit("scanStart")); + this.bleService.on("scanStop", () => this.emit("scanStop")); + // wait for adapter to become ready + let counter = 5; + do { + await sleep(500); + counter--; + } while (counter > 0 && !this.adapterReady); + return this.adapterReady; + } + return true; + } + + stopBTService(): boolean { + if (this.bleService != null) { + this.stopScanLock(); + this.bleService = null; + } + return true; + } + + async startScanLock(): Promise { + if (this.bleService != null) { + return await this.bleService.startScan(); + } + return false; + } + + async stopScanLock(): Promise { + if (this.bleService != null) { + return await this.bleService.stopScan(); + } + return true; + } + + getLockData(): TTLockData[] { + const lockData: TTLockData[] = []; + this.lockData.forEach((lock) => { + lockData.push(lock); + }) + return lockData; + } + + private onScanResult(device: TTBluetoothDevice): void { + // Is it a Lock device ? + if (device.lockType != LockType.UNKNOWN) { + const data = this.lockData.get(device.address); + const lock = new TTLock(device, data); + lock.on("lockUpdated", (lock) => { + const lockData = lock.getLockData(); + if (lockData) { + this.lockData.set(lockData.address, lockData); + } + }); + lock.on("lockReset", (address) => { + this.lockData.delete(address); + }); + this.emit("foundLock", lock); + } + } +} diff --git a/src/api/Command.ts b/src/api/Command.ts new file mode 100644 index 0000000..8054295 --- /dev/null +++ b/src/api/Command.ts @@ -0,0 +1,35 @@ +'use strict'; + +import { CommandResponse } from "../constant/CommandResponse"; +import { CommandType } from "../constant/CommandType"; + +export interface CommandInterface { + readonly COMMAND_TYPE: CommandType; + new(data: Buffer): Command; +} + +export abstract class Command { + protected commandResponse: CommandResponse = CommandResponse.UNKNOWN; + protected commandData?: Buffer; + protected commandRawData?: Buffer; + + constructor(data?: Buffer) { + if (data) { + this.commandResponse = data.readInt8(1); + this.commandData = data.subarray(2); + console.log('Command:', this.commandData.toString("hex")); + this.processData(); + } + } + + getResponse(): CommandResponse { + return this.commandResponse; + } + + getRawData(): Buffer | void { + return this.commandRawData; + } + + protected abstract processData(): void; + abstract build(): Buffer; +} \ No newline at end of file diff --git a/src/api/CommandEnvelope.ts b/src/api/CommandEnvelope.ts new file mode 100644 index 0000000..292552f --- /dev/null +++ b/src/api/CommandEnvelope.ts @@ -0,0 +1,263 @@ +'use strict'; + +import { CommandType } from "../constant/CommandType"; +import { CodecUtils } from "../util/CodecUtils"; +import { LockType, LockVersion } from "../constant/Lock"; +import { AESUtil } from "../util/AESUtil"; +import { Command } from "./Command"; +import { commandFromData, commandFromType } from "./commandBuilder"; + +const DEFAULT_HEADER = Buffer.from([0x7F, 0x5A]); + +export class CommandEnvelope { + static APP_COMMAND: number = 0xaa; + private header: Buffer = DEFAULT_HEADER; + private protocol_type: number = -1; + private sub_version: number = -1; + private scene: number = -1; + private organization: number = -1; + private sub_organization: number = -1; + private commandType: CommandType = -1; + private encrypt: number = 0; + private data?: Buffer; + private lockType: LockType = LockType.UNKNOWN; + private aesKey?: Buffer; + private command?: Command; + private crcok: boolean = true; + + /** + * Create a command from raw data usually received from characteristic change + * @param rawData + */ + static createFromRawData(rawData: Buffer, aesKey?: Buffer): CommandEnvelope { + const command = new CommandEnvelope(); + if (rawData.length < 7) { + throw new Error("Data too short"); + } + command.header = rawData.subarray(0, 2); + command.protocol_type = rawData.readInt8(2); + if (command.protocol_type >= 5) { //New agreement + if (rawData.length < 13) { + throw new Error("New agreement data too short"); + } + command.sub_version = rawData.readInt8(3); + command.scene = rawData.readInt8(4); + command.organization = rawData.readInt16BE(5); // or readInt16LE ? + command.sub_organization = rawData.readInt16BE(7); + command.commandType = rawData.readUInt8(9); + command.encrypt = rawData.readInt8(10); + const length = rawData.readInt8(11); + if (length < 0 || rawData.length < 12 + length + 1) { // header + data + crc + throw new Error("Invalid data length"); + } + if (length > 0) { + command.data = rawData.subarray(12, 12 + length); + } else { + command.data = Buffer.from([]); + } + } else { + command.commandType = rawData.readUInt8(3); + command.encrypt = rawData.readInt8(4); + const length = rawData.readInt8(5); + if (length < 0 || rawData.length < 6 + length + 1) { + throw new Error("Invalid data length"); + } + command.data = rawData.subarray(6, 6 + length); + } + // check CRC + const crc = CodecUtils.crccompute(rawData.slice(0, rawData.length - 1)); + const dataCrc = rawData.readUInt8(rawData.length - 1); + if (dataCrc != crc) { + command.crcok = false; + } + command.generateLockType(); + + if (typeof aesKey != "undefined") { + command.aesKey = aesKey; + // command.generateCommand(); + } + + return command; + } + + /** + * Create new command starting from the version of the device + * @param lockVersion + */ + static createFromLockVersion(lockVersion: LockVersion, aesKey?: Buffer): CommandEnvelope { + const command = new CommandEnvelope; + command.header = DEFAULT_HEADER; + command.protocol_type = lockVersion.getProtocolType(); + command.sub_version = lockVersion.getProtocolVersion(); + command.scene = lockVersion.getScene(); + command.organization = lockVersion.getGroupId(); + command.sub_organization = lockVersion.getOrgId(); + command.encrypt = CommandEnvelope.APP_COMMAND; + command.generateLockType(); + if (aesKey) { + command.setAesKey(aesKey); + } + return command; + } + + static createFromLockType(lockType: LockType, aesKey?: Buffer): CommandEnvelope { + const lockVersion = LockVersion.getLockVersion(lockType); + if (lockVersion != null) { + const command = CommandEnvelope.createFromLockVersion(lockVersion, aesKey); + if (lockType == LockType.LOCK_TYPE_V2) { + command.encrypt = Math.round(Math.random() * 126) + 1; + } + command.generateLockType(); + return command; + } + throw new Error("Invalid lockType"); + } + + private constructor() { + + } + + /** + * Maybe combine with ExtendedBluetoothDevice::getLockType + */ + private generateLockType(): void { + if (this.protocol_type == 0x05 && this.sub_version == 0x03 && this.scene == 0x07) + this.setLockType(LockType.LOCK_TYPE_V3_CAR); + else if (this.protocol_type == 0x0a && this.sub_version == 0x01) + this.setLockType(LockType.LOCK_TYPE_CAR); + else if (this.protocol_type == 0x05 && this.sub_version == 0x03) + this.setLockType(LockType.LOCK_TYPE_V3); + else if (this.protocol_type == 0x05 && this.sub_version == 0x04) + this.setLockType(LockType.LOCK_TYPE_V2S_PLUS); + else if (this.protocol_type == 0x05 && this.sub_version == 0x01) + this.setLockType(LockType.LOCK_TYPE_V2S); + else if (this.protocol_type == 0x0b && this.sub_version == 0x01) + this.setLockType(LockType.LOCK_TYPE_MOBI); + else if (this.protocol_type == 0x03) + this.setLockType(LockType.LOCK_TYPE_V2); + } + + setAesKey(aesKey: Buffer) { + this.aesKey = aesKey; + // this.generateCommand(); + } + + setLockType(lockType: LockType) { + this.lockType = lockType; + } + + getLockType(): LockType { + return this.lockType; + } + + setCommandType(command: CommandType) { + if (typeof command == "string") { + command = command.charCodeAt(0); + } + this.commandType = command; + // this.generateCommand(); + } + + getCommandType(): CommandType { + return this.commandType; + } + + getCommand(): Command { + this.generateCommand(); + if (this.command) { + return this.command; + } else { + throw new Error("Command has not been generated"); + } + } + + isCrcOk(): boolean { + return this.crcok; + } + + private getData(): Buffer { + if (this.data) { + if (this.aesKey) { + return AESUtil.aesDecrypt(this.data, this.aesKey); + } else { + return CodecUtils.decodeWithEncrypt(this.data, this.encrypt); + } + } else { + throw new Error("No data"); + } + } + + buildCommandBuffer(): Buffer { + this.generateCommand(); + if (typeof this.command == "undefined") { + throw new Error("Command has not been generated"); + } + + const data = this.command.build(); + if (typeof this.aesKey == "undefined" && data.length > 0) { + throw new Error("AES key has not been set"); + } + + const org = new ArrayBuffer(4); + const dataView = new DataView(org); + dataView.setInt16(0, this.organization, false); // Bin Endian + dataView.setInt16(2, this.sub_organization, false); // Bin Endian + + let encryptedData: Buffer; + // if there is no data we don't need to encrypt it + if (data.length > 0) { + encryptedData = AESUtil.aesEncrypt(data, this.aesKey); + } else { + encryptedData = data; + } + + let command = Buffer.concat([ + this.header, + Buffer.from([ + this.protocol_type, + this.sub_version, + this.scene + ]), + Buffer.from(org), + Buffer.from([ + this.commandType, + this.encrypt, + encryptedData.length + ]), + encryptedData + ]); + + const crc = CodecUtils.crccompute(command); + command = Buffer.concat([ + command, + Buffer.from([crc]) + ]); + + return command; + } + + /** + * Generate the command from the commandType and data + * + * Command should be built when + * - creating the envelope from data (received command/response) but only after having the aesKey + * - creating a new envelope and we have the commandType and aesKey + * + */ + private generateCommand() { + if (this.commandType != -1 && typeof this.command == "undefined") { + // only generate if no command exists + if (typeof this.data != "undefined") { + if (this.data.length > 0 && typeof this.aesKey != "undefined") { + // create a new command using data + // this is used for receiving command responses or notifications from the lock + this.command = commandFromData(this.getData()); + } + } else { + // create a new blank command from the current commandType + // this is used for sending commands to the lock + this.command = commandFromType(this.commandType); + } + } + } +} \ No newline at end of file diff --git a/src/api/Commands/AESKeyCommand.ts b/src/api/Commands/AESKeyCommand.ts new file mode 100644 index 0000000..7c4dcc4 --- /dev/null +++ b/src/api/Commands/AESKeyCommand.ts @@ -0,0 +1,35 @@ +'use strict'; + +import { CommandType } from "../../constant/CommandType"; +import { Command } from "../Command"; + +export class AESKeyCommand extends Command { + static COMMAND_TYPE: CommandType = CommandType.COMM_GET_AES_KEY; + + private aesKey?: Buffer; + + protected processData() { + if (this.commandData) { + this.setAESKey(this.commandData); + } + } + + build(): Buffer { + if (this.aesKey) { + return Buffer.concat([ + Buffer.from([AESKeyCommand.COMMAND_TYPE, this.commandResponse]), + this.aesKey + ]); + } else { + return Buffer.from("SCIENER"); + } + } + + setAESKey(key: Buffer) { + this.aesKey = key; + } + + getAESKey(): Buffer | void { + return this.aesKey; + } +} \ No newline at end of file diff --git a/src/api/Commands/AddAdminCommand.ts b/src/api/Commands/AddAdminCommand.ts new file mode 100644 index 0000000..d24a767 --- /dev/null +++ b/src/api/Commands/AddAdminCommand.ts @@ -0,0 +1,64 @@ +'use strict'; + +import { CommandResponse } from "../../constant/CommandResponse"; +import { CommandType } from "../../constant/CommandType"; +import { Command } from "../Command"; + +export class AddAdminCommand extends Command { + static COMMAND_TYPE: CommandType = CommandType.COMM_ADD_ADMIN; + private adminPs?: number; + private unlockKey?: number; + + generateNumber(): number { + return Math.floor(Math.random() * 100000000); + } + + setAdminPs(adminPassword?: number): number { + if (adminPassword) { + this.adminPs = adminPassword; + } else { + this.adminPs = this.generateNumber(); + } + return this.adminPs; + } + + getAdminPs(): number | undefined { + return this.adminPs; + } + + setUnlockKey(unlockNumber?: number): number { + if (unlockNumber) { + this.unlockKey = unlockNumber; + } else { + this.unlockKey = this.generateNumber(); + } + return this.unlockKey; + } + + getUnlockKey(): number | undefined { + return this.unlockKey; + } + + protected processData(): void { + const data = this.commandData?.toString(); + if (data != "SCIENER") { + this.commandResponse = CommandResponse.FAILED; + } + } + + build(): Buffer { + if (this.adminPs && this.unlockKey) { + const adminUnlock = new ArrayBuffer(8); + const dataView = new DataView(adminUnlock); + dataView.setUint32(0, this.adminPs, false); // Bin Endian + dataView.setUint32(4, this.unlockKey, false); // Bin Endian + return Buffer.concat([ + Buffer.from(adminUnlock), + Buffer.from("SCIENER"), + ]); + } else { + throw new Error("adminPs and unlockKey were not set"); + } + } + +} \ No newline at end of file diff --git a/src/api/Commands/AudioManageCommand.ts b/src/api/Commands/AudioManageCommand.ts new file mode 100644 index 0000000..95d902f --- /dev/null +++ b/src/api/Commands/AudioManageCommand.ts @@ -0,0 +1,39 @@ +'use strict'; + +import { AudioManage } from "../../constant/AudioManage"; +import { CommandType } from "../../constant/CommandType"; +import { Command } from "../Command"; + +export class AudioManageCommand extends Command { + static COMMAND_TYPE: CommandType = CommandType.COMM_AUDIO_MANAGE; + private opType: AudioManage.QUERY | AudioManage.MODIFY = AudioManage.QUERY; + private opValue?: AudioManage.TURN_ON | AudioManage.TURN_OFF; // lockData.lockSound + + protected processData(): void { + if (this.commandData && this.commandData.length >= 3) { + this.opType = this.commandData.readUInt8(1); + if (this.opType == AudioManage.QUERY && this.commandData.length > 1) { + this.opValue = this.commandData.readUInt8(1); + } + } + } + + build(): Buffer { + if (this.opType == AudioManage.QUERY) { + return Buffer.from([this.opType]); + } else { + return Buffer.from([this.opType, this.opValue]); + } + } + + setNewValue(opValue: AudioManage.TURN_ON | AudioManage.TURN_OFF) { + this.opValue = opValue; + this.opType = AudioManage.MODIFY; + } + + getValue(): AudioManage.TURN_ON | AudioManage.TURN_OFF | void { + if (this.opValue) { + return this.opValue; + } + } +} \ No newline at end of file diff --git a/src/api/Commands/AutoLockManageCommand.ts b/src/api/Commands/AutoLockManageCommand.ts new file mode 100644 index 0000000..74936c1 --- /dev/null +++ b/src/api/Commands/AutoLockManageCommand.ts @@ -0,0 +1,64 @@ +'use strict'; + +import { AutoLockOperate } from "../../constant/AutoLockOperate"; +import { CommandType } from "../../constant/CommandType"; +import { Command } from "../Command"; + +export class AutoLockManageCommand extends Command { + static COMMAND_TYPE: CommandType = CommandType.COMM_AUTO_LOCK_MANAGE; + + private opType: AutoLockOperate.SEARCH | AutoLockOperate.MODIFY = AutoLockOperate.SEARCH; + private opValue?: number; + private batteryCapacity?: number; + + protected processData(): void { + if (this.commandData && this.commandData.length >= 4) { + // 0 - battery + // 1 - opType + // 2,3 - opValue + // 4,5 - min value + // 6,7 - max value + this.batteryCapacity = this.commandData.readUInt8(0); + this.opType = this.commandData.readUInt8(1); + if (this.opType == AutoLockOperate.SEARCH) { + this.opValue = this.commandData.readUInt16BE(2); + } else { + + } + } + } + build(): Buffer { + if (this.opType == AutoLockOperate.SEARCH) { + return Buffer.from([this.opType]); + } else if (this.opValue) { + return Buffer.from([ + this.opType, + this.opValue >> 8, + this.opValue + ]); + } else { + return Buffer.from([]); + } + } + + setTime(opValue: number) { + this.opValue = opValue; + this.opType = AutoLockOperate.MODIFY; + } + + getTime(): number { + if (this.opValue) { + return this.opValue; + } else { + return -1; + } + } + + getBatteryCapacity(): number { + if (this.batteryCapacity) { + return this.batteryCapacity; + } else { + return -1; + } + } +} \ No newline at end of file diff --git a/src/api/Commands/CalibrationTimeCommand.ts b/src/api/Commands/CalibrationTimeCommand.ts new file mode 100644 index 0000000..ca06212 --- /dev/null +++ b/src/api/Commands/CalibrationTimeCommand.ts @@ -0,0 +1,23 @@ +'use strict'; + +import { CommandType } from "../../constant/CommandType"; +import { Command } from "../Command"; +import moment from "moment"; +import { dateTimeToBuffer } from "../../util/timeUtil"; + +export class CalibrationTimeCommand extends Command { + static COMMAND_TYPE: CommandType = CommandType.COMM_TIME_CALIBRATE; + private time?: string; + + protected processData(): void { + // nothing to do here + } + + build(): Buffer { + if (typeof this.time == "undefined") { + this.time = moment().format("YYMMDDHHmmss"); + } + return dateTimeToBuffer(this.time); + } + +} \ No newline at end of file diff --git a/src/api/Commands/CheckAdminCommand.ts b/src/api/Commands/CheckAdminCommand.ts new file mode 100644 index 0000000..d46ee69 --- /dev/null +++ b/src/api/Commands/CheckAdminCommand.ts @@ -0,0 +1,40 @@ +'use strict'; + +import { CommandType } from "../../constant/CommandType"; +import { Command } from "../Command"; + +export class CheckAdminCommand extends Command { + static COMMAND_TYPE: CommandType = CommandType.COMM_CHECK_ADMIN; + + private uid: number = 0; + private adminPs?: number; + private lockFlagPos: number = 0; + + protected processData(): void { + // nothing to do, all incomming data is the 'token' + } + + build(): Buffer { + if (typeof this.adminPs != "undefined") { + const data = Buffer.alloc(11); + data.writeUInt32BE(this.lockFlagPos, 3); // 4 bytes (first one overlaps with adminPs) + data.writeUInt32BE(this.adminPs, 0); // 4 bytes + data.writeUInt32BE(this.uid, 7); + return data; + } else { + return Buffer.from([]); + } + } + + setParams(adminPs: number) { + this.adminPs = adminPs; + } + + getPsFromLock(): number { + if(this.commandData) { + return this.commandData.readUInt32BE(0); + } else { + return -1; + } + } +} \ No newline at end of file diff --git a/src/api/Commands/CheckRandomCommand.ts b/src/api/Commands/CheckRandomCommand.ts new file mode 100644 index 0000000..4431d37 --- /dev/null +++ b/src/api/Commands/CheckRandomCommand.ts @@ -0,0 +1,27 @@ +'use strict'; + +import { CommandType } from "../../constant/CommandType"; +import { Command } from "../Command"; + +export class CheckRandomCommand extends Command { + static COMMAND_TYPE: CommandType = CommandType.COMM_CHECK_RANDOM; + + private sum?: number; + + protected processData(): void { + // nothing to do here + } + + build(): Buffer { + if (this.sum) { + const data = Buffer.alloc(4); + data.writeUInt32BE(this.sum); + return data; + } + return Buffer.from([]); + } + + setSum(psFromLock:number, unlockKey: number) { + this.sum = psFromLock + unlockKey; + } +} \ No newline at end of file diff --git a/src/api/Commands/CheckUserTimeCommand.ts b/src/api/Commands/CheckUserTimeCommand.ts new file mode 100644 index 0000000..8b90dbb --- /dev/null +++ b/src/api/Commands/CheckUserTimeCommand.ts @@ -0,0 +1,45 @@ +'use strict'; + +import { CommandType } from "../../constant/CommandType"; +import { dateTimeToBuffer } from "../../util/timeUtil"; +import { Command } from "../Command"; + +export class CheckUserTimeCommand extends Command { + static COMMAND_TYPE: CommandType = CommandType.COMM_CHECK_USER_TIME; + + private uid?: number; + private startDate?: string; + private endDate?: string; + private lockFlagPos?: number; + + protected processData(): void { + // nothing to do + } + + build(): Buffer { + if (typeof this.uid != "undefined" && this.startDate && this.endDate && typeof this.lockFlagPos != "undefined") { + const data = Buffer.alloc(17); //5+5+3+4 + dateTimeToBuffer(this.startDate).copy(data, 0); + data.writeUInt32BE(this.lockFlagPos, 9); // overlap first byte + dateTimeToBuffer(this.endDate).copy(data, 5); + data.writeUInt32BE(this.uid, 13); + return data; + } + return Buffer.from([]); + } + + setPayload(uid: number, startDate: string, endDate: string, lockFlagPos: number) { + this.uid = uid; + this.startDate = startDate; + this.endDate = endDate; + this.lockFlagPos = lockFlagPos; + } + + getPsFromLock(): number { + if (this.commandData) { + return this.commandData.readUInt32BE(0); + } else { + return -1; + } + } +} \ No newline at end of file diff --git a/src/api/Commands/ControlLampCommand.ts b/src/api/Commands/ControlLampCommand.ts new file mode 100644 index 0000000..3f00951 --- /dev/null +++ b/src/api/Commands/ControlLampCommand.ts @@ -0,0 +1,16 @@ +'use strict'; + +import { CommandType } from "../../constant/CommandType"; +import { Command} from "../Command"; + +export class ControlLampCommand extends Command { + static COMMAND_TYPE: CommandType = CommandType.COMM_LAMP; + + protected processData(): void { + throw new Error("Method not implemented."); + } + build(): Buffer { + throw new Error("Method not implemented."); + } + +} \ No newline at end of file diff --git a/src/api/Commands/ControlRemoteUnlockCommand.ts b/src/api/Commands/ControlRemoteUnlockCommand.ts new file mode 100644 index 0000000..126c481 --- /dev/null +++ b/src/api/Commands/ControlRemoteUnlockCommand.ts @@ -0,0 +1,40 @@ +'use strict'; + +import { CommandType } from "../../constant/CommandType"; +import { ConfigRemoteUnlock } from "../../constant/ConfigRemoteUnlock"; +import { Command } from "../Command"; + +export class ControlRemoteUnlockCommand extends Command { + static COMMAND_TYPE: CommandType = CommandType.COMM_CONTROL_REMOTE_UNLOCK; + + private opType: ConfigRemoteUnlock.OP_TYPE_SEARCH | ConfigRemoteUnlock.OP_TYPE_MODIFY = ConfigRemoteUnlock.OP_TYPE_SEARCH; + private opValue?: ConfigRemoteUnlock.OP_CLOSE | ConfigRemoteUnlock.OP_OPEN; + + protected processData(): void { + if (this.commandData) { + this.opType = this.commandData.readUInt8(0); + if (this.opType == ConfigRemoteUnlock.OP_TYPE_SEARCH && this.commandData.length > 1) { + this.opValue = this.commandData.readUInt8(1); + } + } + } + + build(): Buffer { + if (this.opType == ConfigRemoteUnlock.OP_TYPE_SEARCH) { + return Buffer.from([this.opType]); + } else { + return Buffer.from([this.opType, this.opValue]); + } + } + + setNewValue(opValue: ConfigRemoteUnlock.OP_CLOSE | ConfigRemoteUnlock.OP_OPEN) { + this.opValue = opValue; + this.opType = ConfigRemoteUnlock.OP_TYPE_MODIFY; + } + + getValue(): ConfigRemoteUnlock.OP_CLOSE | ConfigRemoteUnlock.OP_OPEN | void { + if (this.opValue) { + return this.opValue; + } + } +} \ No newline at end of file diff --git a/src/api/Commands/CyclicDateCommand.ts b/src/api/Commands/CyclicDateCommand.ts new file mode 100644 index 0000000..24b01ce --- /dev/null +++ b/src/api/Commands/CyclicDateCommand.ts @@ -0,0 +1,104 @@ +'use strict'; + +import { CommandType } from "../../constant/CommandType"; +import { CyclicOpType } from "../../constant/CyclicOpType"; +import { Command } from "../Command"; +import { CyclicConfig } from "../ValidityInfo"; + +export class CyclicDateCommand extends Command { + static COMMAND_TYPE: CommandType = CommandType.COMM_CYCLIC_CMD; + + private opType?: CyclicOpType; + private userType?: CyclicOpType; + private user?: string; + private cyclicConfig?: CyclicConfig; + + protected processData(): void { + throw new Error("Method not implemented."); + } + + build(): Buffer { + if (typeof this.opType != "undefined") { + switch (this.opType) { + case CyclicOpType.ADD: + case CyclicOpType.CLEAR: + if (typeof this.userType != "undefined" && typeof this.user != "undefined") { + let userLen = this.calculateUserLen(this.userType, this.user); + let data: Buffer; + if (this.opType == CyclicOpType.ADD) { + data = Buffer.alloc(3 + userLen + 11); // why so much, we only requre 7 extra bytes + } else { + data = Buffer.alloc(3 + userLen); + } + switch (userLen) { + case 6: + data.writeBigInt64BE(BigInt(this.user), 1); + break; + case 8: + data.writeBigInt64BE(BigInt(this.user), 3); + break; + case 4: + data.writeInt32BE(parseInt(this.user), 3); + break; + } + data.writeUInt8(this.opType, 0); + data.writeUInt8(this.userType, 1); + data.writeUInt8(userLen, 2); + if (this.opType == CyclicOpType.ADD && typeof this.cyclicConfig != "undefined") { + let index = userLen + 3; + data.writeUInt8(CyclicOpType.CYCLIC_TYPE_WEEK, index++); + data.writeUInt8(this.cyclicConfig.weekDay, index++); + data.writeUInt8(0, index++); + data.writeUInt8(this.cyclicConfig.startTime / 60, index++); + data.writeUInt8(this.cyclicConfig.startTime % 60, index++); + data.writeUInt8(this.cyclicConfig.endTime / 60, index++); + data.writeUInt8(this.cyclicConfig.endTime % 60, index++); + } + return data; + } + break; + default: + throw new Error("opType not implemented"); + } + } + return Buffer.from([]); + } + + addIC(cardNumber: string, cyclicConfig: CyclicConfig) { + this.opType = CyclicOpType.ADD; + this.userType = CyclicOpType.USER_TYPE_IC; + this.user = cardNumber; + this.cyclicConfig = cyclicConfig; + } + + clearIC(cardNumber: string) { + this.opType = CyclicOpType.CLEAR; + this.userType = CyclicOpType.USER_TYPE_IC; + this.user = cardNumber; + } + + addFR(fpNumber: string, cyclicConfig: CyclicConfig) { + this.opType = CyclicOpType.ADD; + this.userType = CyclicOpType.USER_TYPE_FR; + this.user = fpNumber; + this.cyclicConfig = cyclicConfig; + } + + clearFR(fpNumber: string) { + this.opType = CyclicOpType.CLEAR; + this.userType = CyclicOpType.USER_TYPE_FR; + this.user = fpNumber; + } + + private calculateUserLen(userType: CyclicOpType, user: string): number { + let userLen = 8; + if (userType == CyclicOpType.USER_TYPE_FR) { + userLen = 6; + } else { + if (BigInt(user) <= 0xffffffff) { + userLen = 4; + } + } + return userLen; + } +} \ No newline at end of file diff --git a/src/api/Commands/DeviceFeaturesCommand.ts b/src/api/Commands/DeviceFeaturesCommand.ts new file mode 100644 index 0000000..c54f7de --- /dev/null +++ b/src/api/Commands/DeviceFeaturesCommand.ts @@ -0,0 +1,90 @@ +'use strict'; + +import { CommandType } from "../../constant/CommandType"; +import { FeatureValue } from "../../constant/FeatureValue"; +import { padHexString } from "../../util/digitUtil"; +import { Command } from "../Command"; + +export class DeviceFeaturesCommand extends Command { + static COMMAND_TYPE: CommandType = CommandType.COMM_SEARCHE_DEVICE_FEATURE; + + private batteryCapacity?: number; + private special?: number; + private featureList?: Set; + + protected processData(): void { + if (this.commandData) { + this.batteryCapacity = this.commandData.readInt8(0); + this.special = this.commandData.readInt32BE(1); + console.log(this.commandData); + const features = this.commandData.readUInt32BE(1); + this.featureList = this.processFeatures(features); + } + } + + protected readFeatures(data?: Buffer): string { + if (data) { + let features: string = ""; + let temp: string = ""; + for (let i = 0; i < data.length; i++) { + temp += padHexString(data.readInt8(i).toString(16)); + if (i % 4 == 3) { + features = temp + features; + temp = ""; + } + } + let i = 0; + while (i < features.length && features.charAt(i) == "0") { + i++; + } + if (i == features.length) { + return "0"; + } + return features.substring(i).toUpperCase(); + } else { + return ""; + } + } + + protected processFeatures(features: number): Set { + let featureList: Set = new Set(); + const featuresBinary = features.toString(2); + Object.values(FeatureValue).forEach((feature) => { + if (typeof feature != "string" && featuresBinary.length > (feature as number)) { + if (featuresBinary.charAt(featuresBinary.length - (feature as number) - 1) == "1") { + featureList.add(feature as FeatureValue); + } + } + }); + return featureList; + } + + getBatteryCapacity(): number { + if (this.batteryCapacity) { + return this.batteryCapacity; + } else { + return -1; + } + } + + getSpecial(): number { + if (this.special) { + return this.special; + } else { + return 0; + } + } + + getFeaturesList(): Set { + if (this.featureList) { + return this.featureList; + } else { + return new Set(); + } + } + + build(): Buffer { + return Buffer.from([]); + } + +} \ No newline at end of file diff --git a/src/api/Commands/GetAdminCodeCommand.ts b/src/api/Commands/GetAdminCodeCommand.ts new file mode 100644 index 0000000..391ba54 --- /dev/null +++ b/src/api/Commands/GetAdminCodeCommand.ts @@ -0,0 +1,34 @@ +'use strict'; + +import { CommandType } from "../../constant/CommandType"; +import { Command } from "../Command"; + +export class GetAdminCodeCommand extends Command { + static COMMAND_TYPE: CommandType = CommandType.COMM_GET_ADMIN_CODE; + + private adminPasscode?: string; + + protected processData(): void { + if (this.commandData) { + const len = this.commandData.readUInt8(1); + if (len != this.commandData.length - 2) { + console.error("GetAdminCodeCommand: data size (" + this.commandData.length + ") does not match declared length(" + len + ")"); + } + if (len > 0) { + this.adminPasscode = this.commandData.subarray(2, this.commandData.length - 2).toString(); + } else { + this.adminPasscode = ""; + } + } + } + + build(): Buffer { + return Buffer.from([]); + } + + getAdminPasscode(): string | undefined { + if (this.adminPasscode) { + return this.adminPasscode; + } + } +} \ No newline at end of file diff --git a/src/api/Commands/GetKeyboardPasswordsCommand.ts b/src/api/Commands/GetKeyboardPasswordsCommand.ts new file mode 100644 index 0000000..482607f --- /dev/null +++ b/src/api/Commands/GetKeyboardPasswordsCommand.ts @@ -0,0 +1,98 @@ +'use strict'; + +import { CommandType } from "../../constant/CommandType"; +import { KeyboardPwdType } from "../../constant/KeyboardPwdType"; +import { Command } from "../Command"; + +export interface KeyboardPassCode { + type: KeyboardPwdType; + newPassCode: string; + passCode: string; + startDate?: string; + endDate?: string; +} + +export class GetKeyboardPasswordsCommand extends Command { + static COMMAND_TYPE: CommandType = CommandType.COMM_PWD_LIST; + + private sequence?: number; + private passCodes?: KeyboardPassCode[]; + + protected processData(): void { + if (this.commandData && this.commandData.length >= 2) { + const totalLen = this.commandData.readUInt16BE(0); + this.passCodes = []; + if (totalLen > 0) { + this.sequence = this.commandData.readInt16BE(2); + let index = 4; + while (index < this.commandData.length) { + // const len = this.commandData.readUInt8(index++); + index++; + let passCode: KeyboardPassCode = { + type: this.commandData.readUInt8(index++), + newPassCode: "", + passCode: "" + }; + + let codeLen = this.commandData.readUInt8(index++); + passCode.newPassCode = this.commandData.subarray(index, index + codeLen).toString(); + index += codeLen; + + codeLen = this.commandData.readUInt8(index++); + passCode.passCode = this.commandData.subarray(index, index + codeLen).toString(); + index += codeLen; + + passCode.startDate = "20" + this.commandData.readUInt8(index++).toString().padStart(2, '0') // year + + this.commandData.readUInt8(index++).toString().padStart(2, '0') // month + + this.commandData.readUInt8(index++).toString().padStart(2, '0') // day + + this.commandData.readUInt8(index++).toString().padStart(2, '0') // hour + + this.commandData.readUInt8(index++).toString().padStart(2, '0'); // minutes + + switch (passCode.type) { + case KeyboardPwdType.PWD_TYPE_COUNT: + case KeyboardPwdType.PWD_TYPE_PERIOD: + passCode.endDate = "20" + this.commandData.readUInt8(index++).toString().padStart(2, '0') // year + + this.commandData.readUInt8(index++).toString().padStart(2, '0') // month + + this.commandData.readUInt8(index++).toString().padStart(2, '0') // day + + this.commandData.readUInt8(index++).toString().padStart(2, '0') // hour + + this.commandData.readUInt8(index++).toString().padStart(2, '0'); // minutes + break; + case KeyboardPwdType.PWD_TYPE_CIRCLE: + index++; + index++; + } + this.passCodes.push(passCode); + } + } + } + } + + build(): Buffer { + if (typeof this.sequence != "undefined") { + const data = Buffer.alloc(2); + data.writeUInt16BE(this.sequence); + return data; + } else { + return Buffer.from([]); + } + } + + setSequence(sequence: number = 0) { + this.sequence = sequence; + } + + getSequence(): number { + if (this.sequence) { + return this.sequence; + } else { + return -1; + } + } + + getPasscodes(): KeyboardPassCode[] { + if (this.passCodes) { + return this.passCodes; + } + return []; + } +} \ No newline at end of file diff --git a/src/api/Commands/GetSwitchStateCommand.ts b/src/api/Commands/GetSwitchStateCommand.ts new file mode 100644 index 0000000..88a74d8 --- /dev/null +++ b/src/api/Commands/GetSwitchStateCommand.ts @@ -0,0 +1,17 @@ +'use strict'; + +import { CommandType } from "../../constant/CommandType"; +import { Command } from "../Command"; + +export class GetSwitchStateCommand extends Command { + static COMMAND_TYPE: CommandType = CommandType.COMM_SWITCH; + + protected processData(): void { + throw new Error("Method not implemented."); + } + + build(): Buffer { + throw new Error("Method not implemented."); + } + +} \ No newline at end of file diff --git a/src/api/Commands/InitCommand.ts b/src/api/Commands/InitCommand.ts new file mode 100644 index 0000000..6d79fdb --- /dev/null +++ b/src/api/Commands/InitCommand.ts @@ -0,0 +1,17 @@ +'use strict'; + +import { CommandType } from "../../constant/CommandType"; +import { Command } from "../Command"; + +export class InitCommand extends Command { + static COMMAND_TYPE: CommandType = CommandType.COMM_INITIALIZATION; + + protected processData(): void { + // nothing to do + } + + build(): Buffer { + return Buffer.from([]); + } + +} \ No newline at end of file diff --git a/src/api/Commands/InitPasswordsCommand.ts b/src/api/Commands/InitPasswordsCommand.ts new file mode 100644 index 0000000..89d3cb6 --- /dev/null +++ b/src/api/Commands/InitPasswordsCommand.ts @@ -0,0 +1,75 @@ +'use strict'; + +import moment from "moment"; +import { CommandType } from "../../constant/CommandType"; +import { Command } from "../Command"; + +export interface CodeSecret { + code: number, + secret: string +} + +export class InitPasswordsCommand extends Command { + static COMMAND_TYPE: CommandType = CommandType.COMM_INIT_PASSWORDS; + protected pwdInfo?: CodeSecret[]; + + protected processData(): void { + // nothing to do here + } + + build(): Buffer { + this.pwdInfo = this.generateCodeSecret(); + let year = this.calculateYear(); + + // first data byte is the year + let buffers: Buffer[] = [ + Buffer.from([year % 100]), // last 2 digits of the year + ]; + for (let i = 0; i < 10; i++, year++) { + buffers.push(this.combineCodeSecret(this.pwdInfo[i].code, this.pwdInfo[i].secret)); + } + + return Buffer.concat(buffers); + } + + getPwdInfo(): CodeSecret[] | void { + if (this.pwdInfo) { + return this.pwdInfo; + } + } + + private generateCodeSecret(): CodeSecret[] { + let generated: CodeSecret[] = []; + for (let i = 0; i < 10; i++) { + let secret: string = ""; + for (let j = 0; j < 10; j++) { + secret += Math.floor(Math.random() * 10).toString(); + } + generated.push({ + code: Math.floor(Math.random() * 1071), + secret: secret + }); + } + return generated; + } + + private combineCodeSecret(code: number, secret: string): Buffer { + const res = Buffer.alloc(6); + res[0] = code >> 4; + res[1] = code << 4 & 0xFF; + const bigSec = BigInt(secret); + const sec = Buffer.alloc(8); + sec.writeBigUInt64BE(bigSec); + sec.copy(res, 2, 4); + res[1] = res[1] | sec[3]; + return res; + } + + private calculateYear(): number { + if (moment().format("MMDD") == "0101") { // someone does not like 1st of Jan + return parseInt(moment().subtract(1, "years").format("YYYY")); + } else { + return parseInt(moment().format("YYYY")); + } + } +} \ No newline at end of file diff --git a/src/api/Commands/LockCommand.ts b/src/api/Commands/LockCommand.ts new file mode 100644 index 0000000..0d94352 --- /dev/null +++ b/src/api/Commands/LockCommand.ts @@ -0,0 +1,67 @@ +'use strict'; + +import moment from "moment"; +import { CommandType } from "../../constant/CommandType"; +import { Command } from "../Command"; +import { UnlockDataInterface } from "./UnlockCommand"; + +export class LockCommand extends Command { + static COMMAND_TYPE: CommandType = CommandType.COMM_FUNCTION_LOCK; + + private sum?: number; + private uid?: number; + private uniqueid?: number; + private dateTime?: string; + private batteryCapacity?: number; + + protected processData(): void { + if (this.commandData && this.commandData.length > 0) { + this.batteryCapacity = this.commandData.readUInt8(0); + if (this.commandData.length >= 15) { + this.uid = this.commandData.readUInt32BE(1); + this.uniqueid = this.commandData.readUInt32BE(5); + const dateObj = { + year: 2000 + this.commandData.readUInt8(9), + month: this.commandData.readUInt8(10) - 1, + day: this.commandData.readUInt8(11), + hour: this.commandData.readUInt8(12), + minute: this.commandData.readUInt8(13), + second: this.commandData.readUInt8(14) + } + this.dateTime = moment(dateObj).format("YYMMDDHHmmss"); + } + } + } + + build(): Buffer { + if (this.sum) { + const data = Buffer.alloc(8); + data.writeUInt32BE(this.sum, 0); + data.writeUInt32BE(moment().unix(), 4); + return data; + } + return Buffer.from([]); + } + + setSum(psFromLock: number, unlockKey: number) { + this.sum = psFromLock + unlockKey; + } + + getUnlockData(): UnlockDataInterface { + const data: UnlockDataInterface = { + uid: this.uid, + uniqueid: this.uniqueid, + dateTime: this.dateTime, + batteryCapacity: this.batteryCapacity + } + return data; + } + + getBatteryCapacity(): number { + if (this.batteryCapacity) { + return this.batteryCapacity; + } else { + return -1; + } + } +} \ No newline at end of file diff --git a/src/api/Commands/ManageFRCommand.ts b/src/api/Commands/ManageFRCommand.ts new file mode 100644 index 0000000..cd8c742 --- /dev/null +++ b/src/api/Commands/ManageFRCommand.ts @@ -0,0 +1,199 @@ +'use strict'; + +import { CommandType } from "../../constant/CommandType"; +import { ICOperate } from "../../constant/ICOperate"; +import { dateTimeToBuffer } from "../../util/timeUtil"; +import { Command } from "../Command"; + +export interface Fingerprint { + fpNumber: string; + startDate: string; + endDate: string; +} + +export class ManageFRCommand extends Command { + static COMMAND_TYPE: CommandType = CommandType.COMM_FR_MANAGE; + + private opType?: ICOperate; + private sequence?: number; + private fingerprints?: Fingerprint[]; + private fpNumber?: string; + private startDate?: string; + private endDate?: string; + private batteryCapacity?: number; + + protected processData(): void { + if (this.commandData && this.commandData.length > 1) { + this.batteryCapacity = this.commandData.readUInt8(0); + this.opType = this.commandData.readUInt8(1); + switch(this.opType) { + case ICOperate.FR_SEARCH: + this.fingerprints = []; + this.sequence = this.commandData.readInt16BE(2); + let index = 4; + while (index < this.commandData.length) { + let fingerprint: Fingerprint = { + fpNumber: "", + startDate: "", + endDate: "" + }; + + const fp: Buffer = Buffer.alloc(8); + this.commandData.copy(fp, 2, index); + fingerprint.fpNumber = fp.readBigInt64BE().toString(); + index += 6; + + fingerprint.startDate = "20" + this.commandData.readUInt8(index++).toString().padStart(2, '0') // year + + this.commandData.readUInt8(index++).toString().padStart(2, '0') // month + + this.commandData.readUInt8(index++).toString().padStart(2, '0') // day + + this.commandData.readUInt8(index++).toString().padStart(2, '0') // hour + + this.commandData.readUInt8(index++).toString().padStart(2, '0'); // minutes + + fingerprint.endDate = "20" + this.commandData.readUInt8(index++).toString().padStart(2, '0') // year + + this.commandData.readUInt8(index++).toString().padStart(2, '0') // month + + this.commandData.readUInt8(index++).toString().padStart(2, '0') // day + + this.commandData.readUInt8(index++).toString().padStart(2, '0') // hour + + this.commandData.readUInt8(index++).toString().padStart(2, '0'); // minutes + + this.fingerprints.push(fingerprint); + } + break; + case ICOperate.ADD: + let status = this.commandData.readUInt8(2); + this.opType = status; + switch (status) { + case ICOperate.STATUS_ADD_SUCCESS: + // TODO: APICommand.OP_RECOVERY_DATA + const fp: Buffer = Buffer.alloc(8); + this.commandData.copy(fp, 2, 3); + this.fpNumber = fp.readBigInt64BE().toString(); + break; + case ICOperate.STATUS_ENTER_ADD_MODE: + // entered add mode + break; + case ICOperate.STATUS_FR_PROGRESS: + // progress reading fingerprint + break; + case ICOperate.STATUS_FR_RECEIVE_TEMPLATE: + // ready to receive fingerprint template + break; + } + break; + case ICOperate.MODIFY: + break; + case ICOperate.DELETE: + break; + case ICOperate.CLEAR: + break; + } + } + } + + build(): Buffer { + if (typeof this.opType != "undefined") { + switch (this.opType) { + case ICOperate.FR_SEARCH: + if (typeof this.sequence != "undefined") { + const data = Buffer.alloc(3); + data.writeUInt8(this.opType, 0); + data.writeUInt16BE(this.sequence, 1); + return data; + } + break; + case ICOperate.ADD: + case ICOperate.MODIFY: + if (typeof this.fpNumber == "undefined") { + return Buffer.from([this.opType]); + } else { + if (this.fpNumber && this.startDate && this.endDate) { + const data: Buffer = Buffer.alloc(17); + data.writeUInt8(this.opType, 0); + + const fp: Buffer = Buffer.alloc(8); + fp.writeBigInt64BE(BigInt(this.fpNumber)); + fp.copy(data, 1, 2); + + dateTimeToBuffer(this.startDate.substr(2) + this.endDate.substr(2)).copy(data, 7); + + return data; + } + } + break; + case ICOperate.CLEAR: + return Buffer.from([this.opType]); + case ICOperate.DELETE: + if (this.fpNumber) { + const data = Buffer.alloc(7); + data.writeUInt8(this.opType, 0); + + const fp: Buffer = Buffer.alloc(8); + fp.writeBigInt64BE(BigInt(this.fpNumber)); + fp.copy(data, 1, 2); + + return data; + } + break; + } + } + return Buffer.from([]); + } + + getType(): ICOperate { + return this.opType || ICOperate.IC_SEARCH; + } + + getFpNumber(): string { + if (this.fpNumber) { + return this.fpNumber; + } + return ""; + } + + setSequence(sequence: number = 0) { + this.sequence = sequence; + this.opType = ICOperate.FR_SEARCH; + } + + getSequence(): number { + if (this.sequence) { + return this.sequence; + } else { + return -1; + } + } + + setAdd(): void { + this.opType = ICOperate.ADD; + } + + setModify(fpNumber: string, startDate: string, endDate: string): void { + this.fpNumber = fpNumber; + this.startDate = startDate; + this.endDate = endDate; + this.opType = ICOperate.MODIFY; + } + + setDelete(fpNumber: string): void { + this.fpNumber = fpNumber; + this.opType = ICOperate.DELETE; + } + + setClear(): void { + this.opType = ICOperate.CLEAR; + } + + getFingerprints(): Fingerprint[] { + if (this.fingerprints) { + return this.fingerprints; + } + return []; + } + + getBatteryCapacity(): number { + if (this.batteryCapacity) { + return this.batteryCapacity; + } else { + return -1; + } + } +} \ No newline at end of file diff --git a/src/api/Commands/ManageICCommand.ts b/src/api/Commands/ManageICCommand.ts new file mode 100644 index 0000000..564baed --- /dev/null +++ b/src/api/Commands/ManageICCommand.ts @@ -0,0 +1,208 @@ +'use strict'; + +import { CommandType } from "../../constant/CommandType"; +import { ICOperate } from "../../constant/ICOperate"; +import { dateTimeToBuffer } from "../../util/timeUtil"; +import { Command } from "../Command"; + +export interface ICCard { + cardNumber: string; + startDate: string; + endDate: string; +} + +export class ManageICCommand extends Command { + static COMMAND_TYPE: CommandType = CommandType.COMM_IC_MANAGE; + + private opType?: ICOperate; + private sequence?: number; + private cards?: ICCard[]; + private cardNumber?: string; + private startDate?: string; + private endDate?: string; + private batteryCapacity?: number; + + protected processData(): void { + if (this.commandData && this.commandData.length > 1) { + this.batteryCapacity = this.commandData.readUInt8(0); + this.opType = this.commandData.readUInt8(1); + switch(this.opType) { + case ICOperate.IC_SEARCH: + this.cards = []; + this.sequence = this.commandData.readInt16BE(2); + let index = 4; + while (index < this.commandData.length) { + let card: ICCard = { + cardNumber: "", + startDate: "", + endDate: "" + }; + if (this.commandData.length == 24) { + card.cardNumber = this.commandData.readBigUInt64BE(index).toString(); + index += 8; + } else { + card.cardNumber = this.commandData.readUInt32BE(index).toString(); + index += 4; + } + + card.startDate = "20" + this.commandData.readUInt8(index++).toString().padStart(2, '0') // year + + this.commandData.readUInt8(index++).toString().padStart(2, '0') // month + + this.commandData.readUInt8(index++).toString().padStart(2, '0') // day + + this.commandData.readUInt8(index++).toString().padStart(2, '0') // hour + + this.commandData.readUInt8(index++).toString().padStart(2, '0'); // minutes + + card.endDate = "20" + this.commandData.readUInt8(index++).toString().padStart(2, '0') // year + + this.commandData.readUInt8(index++).toString().padStart(2, '0') // month + + this.commandData.readUInt8(index++).toString().padStart(2, '0') // day + + this.commandData.readUInt8(index++).toString().padStart(2, '0') // hour + + this.commandData.readUInt8(index++).toString().padStart(2, '0'); // minutes + + this.cards.push(card); + } + break; + case ICOperate.ADD: + let status = this.commandData.readUInt8(2); + this.opType = status; + switch (status) { + case ICOperate.STATUS_ADD_SUCCESS: + // TODO: APICommand.OP_RECOVERY_DATA + let len = this.commandData.length - 3; + // remaining length should be 4 or 8, but if the last 4 bytes are 0xff they should be ignored + if (len == 4 || this.commandData.readUInt32BE(this.commandData.length - 5).toString(16) == 'ffffffff') { + this.cardNumber = this.commandData.readUInt32BE(3).toString(); + } else { + this.cardNumber = this.commandData?.readBigUInt64BE(3).toString(); + } + break; + case ICOperate.STATUS_ENTER_ADD_MODE: + // entered add mode + break; + + } + break; + case ICOperate.MODIFY: + break; + case ICOperate.DELETE: + break; + case ICOperate.CLEAR: + break; + } + } + } + + build(): Buffer { + if (this.opType) { + switch (this.opType) { + case ICOperate.IC_SEARCH: + if (typeof this.sequence != "undefined") { + let data = Buffer.alloc(3); + data.writeUInt8(this.opType, 0); + data.writeUInt16BE(this.sequence, 1); + return data; + } + break; + case ICOperate.ADD: + case ICOperate.MODIFY: + if (typeof this.cardNumber == "undefined") { + return Buffer.from([this.opType]); + } else { + if (this.cardNumber && this.startDate && this.endDate) { + let data: Buffer; + let index = 0; + if (this.cardNumber.length > 10) { + data = Buffer.alloc(19); + data.writeBigUInt64BE(BigInt(this.cardNumber), 1); + index = 9; + } else { + data = Buffer.alloc(15); + data.writeUInt32BE(parseInt(this.cardNumber), 1); + index = 5; + } + data.writeUInt8(this.opType, 0); + + dateTimeToBuffer(this.startDate.substr(2) + this.endDate.substr(2)).copy(data, index); + + return data; + } + } + break; + case ICOperate.CLEAR: + return Buffer.from([this.opType]); + case ICOperate.DELETE: + if (this.cardNumber) { + if (this.cardNumber.length > 10) { + const data = Buffer.alloc(9); + data.writeUInt8(this.opType, 0); + data.writeBigUInt64BE(BigInt(this.cardNumber), 1); + } else { + const data = Buffer.alloc(5); + data.writeUInt8(this.opType, 0); + data.writeUInt32BE(parseInt(this.cardNumber), 1); + return data; + } + } + break; + } + } + return Buffer.from([]); + } + + getType(): ICOperate { + return this.opType || ICOperate.IC_SEARCH; + } + + getCardNumber(): string { + if (this.cardNumber) { + return this.cardNumber; + } + else return ""; + } + + setSequence(sequence: number = 0) { + this.sequence = sequence; + this.opType = ICOperate.IC_SEARCH; + } + + getSequence(): number { + if (this.sequence) { + return this.sequence; + } else { + return -1; + } + } + + setAdd(): void { + this.opType = ICOperate.ADD; + } + + setModify(cardNumber: string, startDate: string, endDate: string): void { + this.cardNumber = cardNumber; + this.startDate = startDate; + this.endDate = endDate; + this.opType = ICOperate.MODIFY; + } + + setDelete(cardNumber: string): void { + this.cardNumber = cardNumber; + this.opType = ICOperate.DELETE; + } + + setClear(): void { + this.opType = ICOperate.CLEAR; + } + + getCards(): ICCard[] { + if (this.cards) { + return this.cards; + } + return []; + } + + getBatteryCapacity(): number { + if (this.batteryCapacity) { + return this.batteryCapacity; + } else { + return -1; + } + } +} \ No newline at end of file diff --git a/src/api/Commands/ManageKeyboardPasswordCommand.ts b/src/api/Commands/ManageKeyboardPasswordCommand.ts new file mode 100644 index 0000000..82f7540 --- /dev/null +++ b/src/api/Commands/ManageKeyboardPasswordCommand.ts @@ -0,0 +1,193 @@ +'use strict'; + +import moment from "moment"; +import { CommandType } from "../../constant/CommandType"; +import { DateConstant } from "../../constant/DateConstant"; +import { KeyboardPwdType } from "../../constant/KeyboardPwdType"; +import { PwdOperateType } from "../../constant/PwdOperateType"; +import { Command } from "../Command"; + +export class ManageKeyboardPasswordCommand extends Command { + static COMMAND_TYPE: CommandType = CommandType.COMM_MANAGE_KEYBOARD_PASSWORD; + + private opType: PwdOperateType = PwdOperateType.PWD_OPERATE_TYPE_ADD; + private type?: KeyboardPwdType; + private oldPassCode?: string; + private passCode?: string; + private startDate?: moment.Moment; + private endDate?: moment.Moment; + + protected processData(): void { + if (this.commandData && this.commandData.length > 0) { + this.opType = this.commandData.readUInt8(1); + } + } + + build(): Buffer { + if (this.opType) { + switch (this.opType) { + case PwdOperateType.PWD_OPERATE_TYPE_CLEAR: + return Buffer.from([this.opType]); + case PwdOperateType.PWD_OPERATE_TYPE_ADD: + return this.buildAdd(); + case PwdOperateType.PWD_OPERATE_TYPE_REMOVE_ONE: + return this.buildDel(); + case PwdOperateType.PWD_OPERATE_TYPE_MODIFY: + return this.buildEdit(); + } + } + return Buffer.from([]); + } + + getOpType(): PwdOperateType { + return this.opType; + } + + addPasscode(type: KeyboardPwdType, passCode: string, startDate: string = DateConstant.START_DATE_TIME, endDate: string = DateConstant.END_DATE_TIME): boolean { + this.type = type; + if (passCode.length >= 4 && passCode.length <= 9) { + this.oldPassCode = ""; + this.passCode = passCode; + } else { + return false; + } + this.startDate = moment(startDate, "YYYYMMDDHHmm"); + if (!this.startDate.isValid()) { + return false; + } + this.endDate = moment(endDate, "YYYYMMDDHHmm"); + if (!this.endDate.isValid()) { + return false; + } + this.opType = PwdOperateType.PWD_OPERATE_TYPE_ADD; + return true; + } + + updatePasscode(type: KeyboardPwdType, oldPassCode: string, newPassCode: string, startDate: string = DateConstant.START_DATE_TIME, endDate: string = DateConstant.END_DATE_TIME): boolean { + this.type = type; + if (oldPassCode.length >= 4 && oldPassCode.length <= 9) { + this.oldPassCode = oldPassCode; + } else { + return false; + } + if (newPassCode.length >= 4 && newPassCode.length <= 9) { + this.passCode = newPassCode; + } else { + return false; + } + this.startDate = moment(startDate, "YYYYMMDDHHmm"); + if (!this.startDate.isValid()) { + return false; + } + this.endDate = moment(endDate, "YYYYMMDDHHmm"); + if (!this.endDate.isValid()) { + return false; + } + this.opType = PwdOperateType.PWD_OPERATE_TYPE_MODIFY; + return true; + } + + deletePasscode(type: KeyboardPwdType, oldPassCode: string): boolean { + this.type = type; + if (oldPassCode.length >= 4 && oldPassCode.length <= 9) { + this.oldPassCode = oldPassCode; + } else { + return false; + } + this.opType = PwdOperateType.PWD_OPERATE_TYPE_REMOVE_ONE; + return true; + } + + clearAllPasscodes() { + this.opType = PwdOperateType.PWD_OPERATE_TYPE_CLEAR; + } + + private buildAdd(): Buffer { + if (this.type && this.passCode && this.startDate && this.endDate) { + let data: Buffer; + if (this.type == KeyboardPwdType.PWD_TYPE_PERMANENT) { + data = Buffer.alloc(1 + 1 + 1 + this.passCode.length + 5 + 5); + } else { + data = Buffer.alloc(1 + 1 + 1 + this.passCode.length + 5); + } + let index = 0; + data.writeUInt8(this.opType, index++); + data.writeUInt8(this.type, index++); + data.writeUInt8(this.passCode.length, index++); + + for (let i = 0; i < this.passCode.length; i++) { + data.writeUInt8(this.passCode.charCodeAt(i), index++); + } + + data.writeUInt8(parseInt(this.startDate.format("YY")), index++); + data.writeUInt8(parseInt(this.startDate.format("MM")), index++); + data.writeUInt8(parseInt(this.startDate.format("DD")), index++); + data.writeUInt8(parseInt(this.startDate.format("HH")), index++); + data.writeUInt8(parseInt(this.startDate.format("mm")), index++); + + if (this.type != KeyboardPwdType.PWD_TYPE_PERMANENT) { + data.writeUInt8(parseInt(this.endDate.format("YY")), index++); + data.writeUInt8(parseInt(this.endDate.format("MM")), index++); + data.writeUInt8(parseInt(this.endDate.format("DD")), index++); + data.writeUInt8(parseInt(this.endDate.format("HH")), index++); + data.writeUInt8(parseInt(this.endDate.format("mm")), index++); + } + + return data; + } else { + return Buffer.from([]); + } + } + + private buildDel(): Buffer { + if (this.type && this.oldPassCode) { + let data: Buffer = Buffer.alloc(1 + 1 + 1 + this.oldPassCode.length); + let index = 0; + data.writeUInt8(this.opType, index++); + data.writeUInt8(this.type, index++); + data.writeUInt8(this.oldPassCode.length, index++); + + for (let i = 0; i < this.oldPassCode.length; i++) { + data.writeUInt8(this.oldPassCode.charCodeAt(i), index++); + } + + return data; + } else { + return Buffer.from([]); + } + } + + private buildEdit(): Buffer { + if (this.type && this.oldPassCode && this.passCode && this.startDate && this.endDate) { + let data: Buffer = Buffer.alloc(1 + 1 + 1 + this.oldPassCode.length + 1 + this.passCode.length + 5 + 5); + let index = 0; + data.writeUInt8(this.opType, index++); + data.writeUInt8(this.type, index++); + data.writeUInt8(this.oldPassCode.length, index++); + for (let i = 0; i < this.oldPassCode.length; i++) { + data.writeUInt8(this.oldPassCode.charCodeAt(i), index++); + } + + data.writeUInt8(this.passCode.length, index++); + for (let i = 0; i < this.passCode.length; i++) { + data.writeUInt8(this.passCode.charCodeAt(i), index++); + } + + data.writeUInt8(parseInt(this.startDate.format("YY")), index++); + data.writeUInt8(parseInt(this.startDate.format("MM")), index++); + data.writeUInt8(parseInt(this.startDate.format("DD")), index++); + data.writeUInt8(parseInt(this.startDate.format("HH")), index++); + data.writeUInt8(parseInt(this.startDate.format("mm")), index++); + + data.writeUInt8(parseInt(this.endDate.format("YY")), index++); + data.writeUInt8(parseInt(this.endDate.format("MM")), index++); + data.writeUInt8(parseInt(this.endDate.format("DD")), index++); + data.writeUInt8(parseInt(this.endDate.format("HH")), index++); + data.writeUInt8(parseInt(this.endDate.format("mm")), index++); + + return data; + } else { + return Buffer.from([]); + } + } +} \ No newline at end of file diff --git a/src/api/Commands/OperateFinishedCommand.ts b/src/api/Commands/OperateFinishedCommand.ts new file mode 100644 index 0000000..21fe84b --- /dev/null +++ b/src/api/Commands/OperateFinishedCommand.ts @@ -0,0 +1,17 @@ +'use strict'; + +import { CommandType } from "../../constant/CommandType"; +import { Command } from "../Command"; + +export class OperateFinishedCommand extends Command { + static COMMAND_TYPE: CommandType = CommandType.COMM_GET_ALARM_ERRCORD_OR_OPERATION_FINISHED; + + protected processData(): void { + // nothing to do + } + + build(): Buffer { + return Buffer.from([]); + } + +} \ No newline at end of file diff --git a/src/api/Commands/PassageModeCommand.ts b/src/api/Commands/PassageModeCommand.ts new file mode 100644 index 0000000..b71b771 --- /dev/null +++ b/src/api/Commands/PassageModeCommand.ts @@ -0,0 +1,100 @@ +'use strict'; + +import { CommandType } from "../../constant/CommandType"; +import { PassageModeOperate } from "../../constant/PassageModeOperate"; +import { PassageModeType } from "../../constant/PassageModeType"; +import { Command } from "../Command"; + +export interface PassageModeData { + type: PassageModeType; + /** 1..7 (Monday..Sunday) */ + weekOrDay: number; + /** month repeat */ + month: number; + /** HHMM */ + startHour: string; + /** HHMM */ + endHour: string; +} + +export class PassageModeCommand extends Command { + static COMMAND_TYPE: CommandType = CommandType.COMM_CONFIGURE_PASSAGE_MODE; + + opType: PassageModeOperate = PassageModeOperate.QUERY; + sequence?: number; + dataOut?: PassageModeData[]; + dataIn?: PassageModeData; + + protected processData(): void { + if (this.commandData && this.commandData.length > 0) { + this.opType = this.commandData.readInt8(1); + if (this.opType == PassageModeOperate.QUERY) { + this.sequence = this.commandData.readInt8(2); + this.dataOut = []; + let index = 3; + if (this.commandData.length >= 10) { + { + this.dataOut.push({ + type: this.commandData.readInt8(index), + weekOrDay: this.commandData.readInt8(index + 1), + month: this.commandData.readInt8(index + 2), + startHour: this.commandData.readInt8(index + 3).toString().padStart(2, '0') + this.commandData.readInt8(index + 4).toString().padStart(2, '0'), + endHour: this.commandData.readInt8(index + 5).toString().padStart(2, '0') + this.commandData.readInt8(index + 6).toString().padStart(2, '0'), + }); + index += 7; + } while (index < this.commandData.length); + } + } else { + + } + } + } + + build(): Buffer { + if (this.opType == PassageModeOperate.QUERY) { + return Buffer.from([this.opType, this.sequence]); + } else if (this.dataIn) { + return Buffer.from([ + this.opType, + this.dataIn.type, + this.dataIn.weekOrDay, + this.dataIn.month, + parseInt(this.dataIn.startHour.substr(0, 2)), + parseInt(this.dataIn.startHour.substr(2, 2)), + parseInt(this.dataIn.endHour.substr(0, 2)), + parseInt(this.dataIn.endHour.substr(2, 2)) + ]); + } else { + return Buffer.from([this.opType]); + } + } + + setSequence(sequence: number = 0) { + this.sequence = sequence; + } + + getSequence(): number { + if (this.sequence) { + return this.sequence; + } else { + return -1; + } + } + + setData(data: PassageModeData, type: PassageModeOperate.ADD | PassageModeOperate.DELETE = PassageModeOperate.ADD) { + this.opType = type; + this.dataIn = data; + } + + setClear() { + this.opType = PassageModeOperate.CLEAR; + } + + getData(): PassageModeData[] { + if (this.dataOut) { + return this.dataOut; + } else { + return [] + } + } +} \ No newline at end of file diff --git a/src/api/Commands/ReadDeviceInfoCommand.ts b/src/api/Commands/ReadDeviceInfoCommand.ts new file mode 100644 index 0000000..c76b557 --- /dev/null +++ b/src/api/Commands/ReadDeviceInfoCommand.ts @@ -0,0 +1,29 @@ +'use strict'; + +import { CommandType } from "../../constant/CommandType"; +import { DeviceInfoEnum } from "../../constant/DeviceInfoEnum"; +import { Command } from "../Command"; + +export class ReadDeviceInfoCommand extends Command { + static COMMAND_TYPE: CommandType = CommandType.COMM_READ_DEVICE_INFO; + private opType: DeviceInfoEnum = DeviceInfoEnum.MODEL_NUMBER; + + protected processData(): void { + // nothing to do here + } + + setInfoType(infoType: DeviceInfoEnum) { + this.opType = infoType; + } + + getInfoData(): Buffer | void { + if (this.commandData) { + return this.commandData.subarray(0, this.commandData.length - 1); + } + } + + build(): Buffer { + return Buffer.from([this.opType]); + } + +} \ No newline at end of file diff --git a/src/api/Commands/ResetLockCommand.ts b/src/api/Commands/ResetLockCommand.ts new file mode 100644 index 0000000..9d87474 --- /dev/null +++ b/src/api/Commands/ResetLockCommand.ts @@ -0,0 +1,16 @@ +'use strict'; + +import { CommandType } from "../../constant/CommandType"; +import { Command } from "../Command"; + +export class ResetLockCommand extends Command { + static COMMAND_TYPE: CommandType = CommandType.COMM_RESET_LOCK; + + protected processData(): void { + // nothing to do here + } + + build(): Buffer { + return Buffer.from([]); + } +} \ No newline at end of file diff --git a/src/api/Commands/ScreenPasscodeManageCommand.ts b/src/api/Commands/ScreenPasscodeManageCommand.ts new file mode 100644 index 0000000..eecd1d0 --- /dev/null +++ b/src/api/Commands/ScreenPasscodeManageCommand.ts @@ -0,0 +1,44 @@ +'use strict'; + +import { ActionType } from "../../constant/ActionType"; +import { CommandType } from "../../constant/CommandType"; +import { Command } from "../Command"; + +export class ScreenPasscodeManageCommand extends Command { + static COMMAND_TYPE: CommandType = CommandType.COMM_SHOW_PASSWORD; + + private opType: ActionType.GET | ActionType.SET = ActionType.GET; + private opValue?: 0 | 1; // lockData.displayPasscode + + protected processData(): void { + if (this.commandData) { + this.opType = this.commandData.readUInt8(0); + if (this.opType == ActionType.GET && this.commandData.length > 1) { + if (this.commandData.readUInt8(1) == 1) { + this.opValue = 1; + } else { + this.opValue = 0; + } + } + } + } + + build(): Buffer { + if (this.opType == ActionType.GET) { + return Buffer.from([this.opType]); + } else { + return Buffer.from([this.opType, this.opValue]); + } + } + + setNewValue(opValue: 0 | 1) { + this.opValue = opValue; + this.opType = ActionType.SET; + } + + getValue(): 0 | 1 | void { + if (this.opValue) { + return this.opValue; + } + } +} \ No newline at end of file diff --git a/src/api/Commands/SearchBicycleStatusCommand.ts b/src/api/Commands/SearchBicycleStatusCommand.ts new file mode 100644 index 0000000..d405e28 --- /dev/null +++ b/src/api/Commands/SearchBicycleStatusCommand.ts @@ -0,0 +1,29 @@ +'use strict'; + +import { CommandType } from "../../constant/CommandType"; +import { Command } from "../Command"; + +export class SearchBicycleStatusCommand extends Command { + static COMMAND_TYPE: CommandType = CommandType.COMM_SEARCH_BICYCLE_STATUS; + + private lockStatus?: number; + + protected processData(): void { + if (this.commandData && this.commandData.length >= 2) { + this.lockStatus = this.commandData.readInt8(1); + } + } + + build(): Buffer { + return Buffer.from("SCIENER"); + } + + getLockStatus(): number { + if (typeof this.lockStatus != "undefined") { + return this.lockStatus; + } else { + return -1; + } + } + +} \ No newline at end of file diff --git a/src/api/Commands/SetAdminKeyboardPwdCommand.ts b/src/api/Commands/SetAdminKeyboardPwdCommand.ts new file mode 100644 index 0000000..308fe78 --- /dev/null +++ b/src/api/Commands/SetAdminKeyboardPwdCommand.ts @@ -0,0 +1,38 @@ +'use strict'; + +import { CommandType } from "../../constant/CommandType"; +import { Command } from "../Command"; + +export class SetAdminKeyboardPwdCommand extends Command { + static COMMAND_TYPE: CommandType = CommandType.COMM_SET_ADMIN_KEYBOARD_PWD; + + private adminPasscode?: string; + + protected processData(): void { + // do nothing yet, we don't know if the lock returns anything + if(this.commandData && this.commandData.length > 0) { + console.log("SetAdminKeyboardPwdCommand received:", this.commandData); + } + // throw new Error("Method not implemented."); + } + + build(): Buffer { + if (this.adminPasscode) { + const data = Buffer.alloc(this.adminPasscode.length); + for (let i = 0; i < this.adminPasscode.length; i++) { + data[i] = parseInt(this.adminPasscode.charAt(i)); + } + return data; + } else { + return Buffer.from([]); + } + } + + setAdminPasscode(adminPasscode: string) { + this.adminPasscode = adminPasscode; + } + + getAdminPasscode(): string | void { + return this.adminPasscode; + } +} \ No newline at end of file diff --git a/src/api/Commands/UnknownCommand.ts b/src/api/Commands/UnknownCommand.ts new file mode 100644 index 0000000..0920060 --- /dev/null +++ b/src/api/Commands/UnknownCommand.ts @@ -0,0 +1,16 @@ +'use strict'; + +import { Command } from "../Command"; + +export class UnknownCommand extends Command { + + protected processData(): void { + if (this.commandData) { + console.error("Unknown command type 0x" + this.commandData.readInt8().toString(16), "succes", this.commandResponse, "data", this.commandData.toString("hex")); + } + } + + build(): Buffer { + throw new Error("Method not implemented."); + } +} \ No newline at end of file diff --git a/src/api/Commands/UnlockCommand.ts b/src/api/Commands/UnlockCommand.ts new file mode 100644 index 0000000..2fb2400 --- /dev/null +++ b/src/api/Commands/UnlockCommand.ts @@ -0,0 +1,73 @@ +'use strict'; + +import moment from "moment"; +import { CommandType } from "../../constant/CommandType"; +import { Command } from "../Command"; + +export interface UnlockDataInterface { + uid?: number; + uniqueid?: number; + dateTime?: string; + batteryCapacity?: number; +} + +export class UnlockCommand extends Command { + static COMMAND_TYPE: CommandType = CommandType.COMM_UNLOCK; + + private sum?: number; + private uid?: number; + private uniqueid?: number; + private dateTime?: string; + private batteryCapacity?: number; + + protected processData(): void { + if (this.commandData && this.commandData.length > 0) { + this.batteryCapacity = this.commandData.readUInt8(0); + if (this.commandData.length >= 15) { + this.uid = this.commandData.readUInt32BE(1); + this.uniqueid = this.commandData.readUInt32BE(5); + const dateObj = { + year: 2000 + this.commandData.readUInt8(9), + month: this.commandData.readUInt8(10) - 1, + day: this.commandData.readUInt8(11), + hour: this.commandData.readUInt8(12), + minute: this.commandData.readUInt8(13), + second: this.commandData.readUInt8(14) + } + this.dateTime = moment(dateObj).format("YYMMDDHHmmss"); + } + } + } + + build(): Buffer { + if (this.sum) { + const data = Buffer.alloc(8); + data.writeUInt32BE(this.sum, 0); + data.writeUInt32BE(moment().unix(), 4); + return data; + } + return Buffer.from([]); + } + + setSum(psFromLock: number, unlockKey: number) { + this.sum = psFromLock + unlockKey; + } + + getUnlockData(): UnlockDataInterface { + const data: UnlockDataInterface = { + uid: this.uid, + uniqueid: this.uniqueid, + dateTime: this.dateTime, + batteryCapacity: this.batteryCapacity + } + return data; + } + + getBatteryCapacity(): number { + if (this.batteryCapacity) { + return this.batteryCapacity; + } else { + return -1; + } + } +} \ No newline at end of file diff --git a/src/api/Commands/index.ts b/src/api/Commands/index.ts new file mode 100644 index 0000000..7f4e9fa --- /dev/null +++ b/src/api/Commands/index.ts @@ -0,0 +1,32 @@ +'use strict'; + +export { UnknownCommand } from "./UnknownCommand"; +export { InitCommand } from "./InitCommand"; +export { AESKeyCommand } from "./AESKeyCommand"; +export { AddAdminCommand } from "./AddAdminCommand"; +export { CalibrationTimeCommand } from "./CalibrationTimeCommand"; +export { DeviceFeaturesCommand } from "./DeviceFeaturesCommand"; +export { GetSwitchStateCommand } from "./GetSwitchStateCommand"; +export { AudioManageCommand } from "./AudioManageCommand"; +export { ScreenPasscodeManageCommand } from "./ScreenPasscodeManageCommand"; +export { AutoLockManageCommand } from "./AutoLockManageCommand"; +export { ControlLampCommand } from "./ControlLampCommand"; +export { SetAdminKeyboardPwdCommand } from "./SetAdminKeyboardPwdCommand"; +export { InitPasswordsCommand } from "./InitPasswordsCommand"; +export { ControlRemoteUnlockCommand } from "./ControlRemoteUnlockCommand"; +export { OperateFinishedCommand } from "./OperateFinishedCommand"; +export { ReadDeviceInfoCommand } from "./ReadDeviceInfoCommand"; +export { GetAdminCodeCommand } from "./GetAdminCodeCommand"; +export { CheckAdminCommand } from "./CheckAdminCommand"; +export { CheckRandomCommand } from "./CheckRandomCommand"; +export { ResetLockCommand } from "./ResetLockCommand"; +export { CheckUserTimeCommand } from "./CheckUserTimeCommand"; +export { UnlockCommand, UnlockDataInterface } from "./UnlockCommand"; +export { LockCommand } from "./LockCommand"; +export { PassageModeCommand, PassageModeData } from "./PassageModeCommand"; +export { SearchBicycleStatusCommand } from "./SearchBicycleStatusCommand"; +export { ManageKeyboardPasswordCommand } from "./ManageKeyboardPasswordCommand"; +export { GetKeyboardPasswordsCommand, KeyboardPassCode } from "./GetKeyboardPasswordsCommand"; +export { ManageICCommand, ICCard } from "./ManageICCommand"; +export { CyclicDateCommand } from "./CyclicDateCommand"; +export { ManageFRCommand, Fingerprint } from "./ManageFRCommand"; \ No newline at end of file diff --git a/src/api/ValidityInfo.ts b/src/api/ValidityInfo.ts new file mode 100644 index 0000000..fc95a48 --- /dev/null +++ b/src/api/ValidityInfo.ts @@ -0,0 +1,99 @@ +'use strict'; + +import moment from "moment"; +import { DateConstant } from "../constant/DateConstant"; + +export enum ValidityType { + TIMED = 1, + CYCLIC = 4 +} + +export interface CyclicConfig { + /** 1-7 monday-sunday */ + weekDay: number, + /** minute of the day for start (Ex: 02:14 = 2*60 + 14 = 134) */ + startTime: number; + /** minute of the day to end (Ex: 16:48 = 16*60 + 48 = 1008) */ + endTime: number; +} + +export class ValidityInfo { + private type: ValidityType; + private startDate: moment.Moment; + private endDate: moment.Moment; + private cycles: CyclicConfig[]; + + constructor(type: ValidityType = ValidityType.TIMED, startDate: string = DateConstant.START_DATE_TIME, endDdate: string = DateConstant.END_DATE_TIME) { + this.type = type; + this.startDate = moment(startDate, "YYYYMMDDHHmm"); + if (!this.startDate.isValid()) { + throw new Error("Invalid startDate"); + } + this.endDate = moment(endDdate, "YYYYMMDDHHmm"); + if (!this.endDate.isValid()) { + throw new Error("Invalid endDate"); + } + this.cycles = []; + } + + setType(type: ValidityType) { + this.type = type; + } + + addCycle(cycle: CyclicConfig): boolean { + if (this.isValidCycle(cycle)) { + this.cycles.push(cycle); + return true; + } + return false; + } + + setStartDate(startDate: string): boolean { + let date = moment(startDate, "YYYYMMDDHHmm"); + if (date.isValid()) { + this.startDate = date; + return true; + } + return false; + } + + setEndDate(endDate: string): boolean { + let date = moment(endDate, "YYYYMMDDHHmm"); + if (date.isValid()) { + this.endDate = date; + return true; + } + return false; + } + + getType(): ValidityType { + return this.type; + } + + getStartDate(): string { + return this.startDate.format("YYYYMMDDHHmm"); + } + + getStartDateMoment(): moment.Moment { + return this.startDate; + } + + getEndDate(): string { + return this.endDate.format("YYYYMMDDHHmm"); + } + + geetEndDateMoment(): moment.Moment { + return this.endDate; + } + + getCycles(): CyclicConfig[] { + return this.cycles; + } + + isValidCycle(cycle: CyclicConfig) { + if (cycle.weekDay < 1 || cycle.weekDay > 7) return false; + if (cycle.startTime < 0 || cycle.startTime > 24 * 60) return false; + if (cycle.endTime < 0 || cycle.endTime > 24 * 60) return false; + return true; + } +} \ No newline at end of file diff --git a/src/api/commandBuilder.ts b/src/api/commandBuilder.ts new file mode 100644 index 0000000..3754cc4 --- /dev/null +++ b/src/api/commandBuilder.ts @@ -0,0 +1,46 @@ +'use strict'; + +import { CommandType } from "../constant/CommandType"; +import { Command, CommandInterface } from "./Command"; +import * as commands from "./Commands"; + +// TODO: index commands based on COMMAND_TYPE for faster lookup + +function getCommandClass(commandType: CommandType): CommandInterface | void { + let commandTypeInt = commandType; + if (typeof commandTypeInt == "string") { + commandTypeInt = commandTypeInt.charCodeAt(0); + } + const classNames = Object.keys(commands); + for (let i = 0; i < classNames.length; i++) { + if (classNames[i] != "UnknownCommand") { + const commandClass: CommandInterface = Reflect.get(commands, classNames[i]); + let cmdTypeInt = commandClass.COMMAND_TYPE; + if (typeof cmdTypeInt == 'string') { + cmdTypeInt = cmdTypeInt.charCodeAt(0); + } + if (cmdTypeInt == commandTypeInt) { + return commandClass; + } + } + } +} + +export function commandFromData(data: Buffer): Command { + const commandType: CommandType = data.readUInt8(0); + const commandClass = getCommandClass(commandType); + if (commandClass) { + return Reflect.construct(commandClass, [data]); + } else { + return new commands.UnknownCommand(data); + } +} + +export function commandFromType(commandType: CommandType): Command { + const commandClass = getCommandClass(commandType); + if (commandClass) { + return Reflect.construct(commandClass, []); + } else { + return new commands.UnknownCommand(); + } +} \ No newline at end of file diff --git a/src/constant/APICommand.ts b/src/constant/APICommand.ts new file mode 100644 index 0000000..27b7ca8 --- /dev/null +++ b/src/constant/APICommand.ts @@ -0,0 +1,235 @@ +'use strict'; + +export enum APICommand { + OP_GET_LOCK_VERSION = 1, + OP_ADD_ADMIN = 2, //Add administrator + OP_UNLOCK_ADMIN = 3, //The administrator opens the door + OP_UNLOCK_EKEY = 4, //Guantong users open the door + OP_SET_KEYBOARD_PASSWORD = 5, //Set the administrator keyboard password + OP_CALIBRATE_TIME = 6, + OP_SET_NORMAL_USER_PASSWORD = 7, //Set delete password + OP_READ_NORMAL_USER_PASSWORD = 8, + OP_CLEAR_NORMAL_USER_PASSWORD = 9, + OP_REMOVE_SINGLE_NORMAL_USER_PASSWORD = 10, + OP_RESET_KEYBOARD_PASSWORD = 11, //Reset keyboard password + OP_SET_DELETE_PASSWORD = 12, + OP_LOCK_ADMIN = 13, //Parking lock admin closes the lock + OP_LOCK_EKEY = 14, //Parking lock EKEY close lock + OP_RESET_EKEY = 15, //set lockFlag + + /** + * Initialization password + */ + OP_INIT_PWD = 16, + + //Set the lock name + OP_SET_LOCK_NAME = 17, + + //Read door lock time + OP_GET_LOCK_TIME = 18, + + //reset + OP_RESET_LOCK = 19, + + /** + * Add a one-time password, start time and end time are required + */ + OP_ADD_ONCE_KEYBOARD_PASSWORD = 20, + + /** + * Add permanent keyboard password, need start time + */ + OP_ADD_PERMANENT_KEYBOARD_PASSWORD = 21, + + /** + * Add period password + */ + OP_ADD_PERIOD_KEYBOARD_PASSWORD = 22, + + /** + * change Password + */ + OP_MODIFY_KEYBOARD_PASSWORD = 23, + + /** + * Delete a single password + */ + OP_REMOVE_ONE_PASSWORD = 24, + + /** + * Delete all passwords in the lock + */ + OP_REMOVE_ALL_KEYBOARD_PASSWORD = 25, + + /** + * Get operation log + */ + OP_GET_OPERATE_LOG = 26, + + /** + * Query device characteristics + */ + OP_SEARCH_DEVICE_FEATURE = 27, + + /** + * Query IC card number + */ + OP_SEARCH_IC_CARD_NO = 28, + + /** + * Add IC card + */ + OP_ADD_IC = 29, + + /** + * Modify the validity period of the IC card + */ + OP_MODIFY_IC_PERIOD = 30, + + /** + * Delete IC card + */ + OP_DELETE_IC = 31, + + /** + * Clear IC card + */ + OP_CLEAR_IC = 32, + + /** + * Set the bracelet KEY + */ + OP_SET_WRIST_KEY = 33, + + /** + * Add fingerprint + */ + OP_ADD_FR = 34, + + /** + * Modify fingerprint validity period + */ + OP_MODIFY_FR_PERIOD = 35, + + /** + * Delete fingerprint + */ + OP_DELETE_FR = 36, + + /** + * Clear fingerprint + */ + OP_CLEAR_FR = 37, + + /** + * Query the shortest and longest lockout time + */ + OP_SEARCH_AUTO_LOCK_PERIOD = 38, + + /** + * Set the blocking time + */ + OP_SET_AUTO_LOCK_TIME = 39, + + /** + * Enter upgrade mode + */ + OP_ENTER_DFU_MODE = 40, + + /** + * Delete passwords in batch + */ + OP_BATCH_DELETE_PASSWORD = 41, + + /** + * Locking function + */ + OP_LOCK = 42, + + /** + * Show hidden password + */ + OP_SHOW_PASSWORD_ON_SCREEN = 43, + + /** + * Data recovery + */ + OP_RECOVERY_DATA = 44, + + /** + * Read password parameters + */ + OP_READ_PWD_PARA = 45, + + + /** + * Query fingerprint list + */ + OP_SEARCH_FR = 46, + + /** + * Query password list + */ + OP_SEARCH_PWD = 47, + + /** + * Control remote unlock switch + */ + OP_CONTROL_REMOTE_UNLOCK = 48, + + /** + * Get battery + */ + OP_GET_POW = 49, + + OP_AUDIO_MANAGEMENT = 50, + + OP_REMOTE_CONTROL_DEVICE_MANAGEMENT = 51, + + /** + * Door sensor operation + */ + OP_DOOR_SENSOR = 52, + + /** + * Detection door sensor + */ + OP_DETECT_DOOR_SENSOR = 53, + + /** + * Get lock switch status + */ + OP_GET_LOCK_SWITCH_STATE = 54, + + /** + * Read device information + */ + OP_GET_DEVICE_INFO = 55, + + /** + * Configure NB lock server address + */ + OP_CONFIGURE_NB_SERVER_ADDRESS = 56, + + OP_GET_ADMIN_KEYBOARD_PASSWORD = 57, //Read the administrator keyboard password + + OP_WRITE_FR = 58, //Write fingerprint data + + OP_QUERY_PASSAGE_MODE = 59, + OP_ADD_OR_MODIFY_PASSAGE_MODE = 60, + OP_DELETE_PASSAGE_MODE = 61, + OP_CLEAR_PASSAGE_MODE = 62, + OP_FREEZE_LOCK = 63, + OP_LOCK_LAMP = 64, + OP_SET_HOTEL_DATA = 65, + OP_SET_SWITCH = 66, + OP_GET_SWITCH = 67, + OP_SET_HOTEL_CARD_SECTION = 68, + OP_DEAD_LOCK = 69, + OP_SET_ELEVATOR_CONTROL_FLOORS = 70, + OP_SET_ELEVATOR_WORK_MODE = 71, + OP_SET_NB_ACTIVATE_CONFIG = 72, + OP_GET_NB_ACTIVATE_CONFIG = 73, + OP_SET_NB_ACTIVATE_MODE = 74, + OP_GET_NB_ACTIVATE_MODE = 75, +} diff --git a/src/constant/ActionType.ts b/src/constant/ActionType.ts new file mode 100644 index 0000000..cc48b68 --- /dev/null +++ b/src/constant/ActionType.ts @@ -0,0 +1,6 @@ +'use strict'; + +export enum ActionType { + GET = 1, + SET = 2, +} \ No newline at end of file diff --git a/src/constant/AudioManage.ts b/src/constant/AudioManage.ts new file mode 100644 index 0000000..3e62734 --- /dev/null +++ b/src/constant/AudioManage.ts @@ -0,0 +1,8 @@ +'use strict'; + +export enum AudioManage { + QUERY = 1, + MODIFY = 2, + TURN_ON = 1, + TURN_OFF = 0, +} \ No newline at end of file diff --git a/src/constant/AutoLockOperate.ts b/src/constant/AutoLockOperate.ts new file mode 100644 index 0000000..d692fcf --- /dev/null +++ b/src/constant/AutoLockOperate.ts @@ -0,0 +1,12 @@ +'use strict'; + +export enum AutoLockOperate { + /** + * Query blocking time + */ + SEARCH = 0x01, + /** + * Modify the blocking time + */ + MODIFY = 0x02, +} \ No newline at end of file diff --git a/src/constant/CallbackOperationType.ts b/src/constant/CallbackOperationType.ts new file mode 100644 index 0000000..74f2384 --- /dev/null +++ b/src/constant/CallbackOperationType.ts @@ -0,0 +1,74 @@ +'use strict'; + +export enum CallbackOperationType { + + UNKNOWN_TYPE = -1, + + INIT_LOCK = 2, + RESET_LOCK = 3, + CONTROL_LOCK = 4, + RESET_KEY = 5, + GET_MUTE_MODE_STATE = 6, + SET_MUTE_MODE_STATE = 7, + GET_REMOTE_UNLOCK_STATE = 8, + SET_REMOTE_UNLOCK_STATE = 9, + GET_PASSCODE_VISIBLE_STATE = 10, + SET_PASSCODE_VISIBLE_STATE = 11, + SET_PASSAGE_MODE = 12, + DELETE_PASSAGE_MODE = 13, + CLEAR_PASSAGE_MODE = 14, + GET_PASSAGE_MODE = 15, + SET_LOCK_TIME = 16, + GET_LOCK_TIME = 17, + GET_OPERATION_LOG = 18, + GET_ELECTRIC_QUALITY = 19, + GET_LOCK_VERSION = 20, + GET_SPECIAL_VALUE = 21, + RECOVERY_DATA = 22, + GET_SYSTEM_INFO = 23, + CREATE_CUSTOM_PASSCODE = 24, + GET_LOCK_STATUS = 25, + SET_AUTO_LOCK_PERIOD = 26, + MODIFY_PASSCODE = 27, + DELETE_PASSCODE = 28, + RESET_PASSCODE = 29, + GET_ALL_VALID_PASSCODES = 30, + GET_PASSCODE_INFO = 31, + MODIFY_ADMIN_PASSCODE = 32, + GET_ADMIN_PASSCODE = 33, + ADD_IC_CARD = 34, + MODIFY_IC_CARD_PERIOD = 35, + ADD_FINGERPRINT = 36, + MODIFY_FINGEPRINT_PERIOD = 37, + GET_ALL_IC_CARDS = 38, + DELETE_IC_CARD = 39, + CLEAR_ALL_IC_CARD = 40, + GET_ALL_FINGERPRINTS = 41, + DELETE_FINGERPRINT = 42, + CLEAR_ALL_FINGERPRINTS = 43, + WRITE_FINGERPRINT_DATA = 44, + ENTER_DFU_MODE = 45, + SET_NB_SERVER = 46, + INIT_KEYPAD = 47, + GET_LOCK_FREEZE_STATE = 48, + SET_LOCK_FREEZE_STATE = 49, + GET_LIGHT_TIME = 50, + SET_LIGHT_TIME = 51, + SET_HOTEL_CARD_SECTION = 52, + CONNECT_LOCK = 53, + SET_LOCK_CONFIG = 54, + GET_LOCK_CONFIG = 55, + SET_HOTEL_DATA = 56, + SET_ELEVATOR_CONTROLABLE_FLOORS = 57, + SET_ELEVATOR_WORK_MODE = 58, + GET_AUTO_LOCK_PERIOD = 59, + + ADD_CYCLIC_IC_CARD = 60, + ADD_CYCLIC_FINGERPRINT = 61, + + SET_NB_ACTIVATE_CONFIG = 62, + GET_NB_ACTIVATE_CONFIG = 63, + SET_NB_ACTIVATE_MODE = 64, + GET_NB_ACITATE_MODE = 65, + +} \ No newline at end of file diff --git a/src/constant/CommandResponse.ts b/src/constant/CommandResponse.ts new file mode 100644 index 0000000..aea0f32 --- /dev/null +++ b/src/constant/CommandResponse.ts @@ -0,0 +1,37 @@ +'use strict'; + +export enum CommandResponse { + UNKNOWN = -1, + SUCCESS = 0X01, + FAILED = 0X00, + +// INITIALIZED = 0X01, +// NOT_INITIALIZED = 0X00, +// +// // by wan ------ 区分F,G +//// MODE_ADMIN = 0X00, +//// MODE_UNLOCK = 0X01, +// +// ADMIN = 0X01, //管理员 +// USER = 0X00, //普通用户 +// +// SUCCESS_GET_DYN_PASSWORD = 0X02, +// ERROR_NONE = 0X00, +// ERROR_INVALID_CRC = 0X01, +// ERROR_NO_PERMISSION = 0X02, +// ERROR_WRONG_ID_OR_PASSWORD = 0X03, +// ERROR_REACH_LIMIT = 0X04, +// ERROR_IN_SETTING = 0X05, +// // ------by wan------ +// ERROR_IN_SAME_USERID = 0X06, // 不能添加重名的 +// ERROR_NO_ADMIN_YET = 0X07, // 必须先添加一个管理员 +// +// ERROR_Dyna_Password_Out_Time = 0X08, // 动态密码过期 +// ERROR_NO_DATA = 0X09, // 数据为空 +// +// ERROR_LOCK_NO_POWER = 0X0a, // 锁没有电量了 +// +// public byte command, +// public byte isSuccess, +// public byte errorCode, +} \ No newline at end of file diff --git a/src/constant/CommandType.ts b/src/constant/CommandType.ts new file mode 100644 index 0000000..75bd450 --- /dev/null +++ b/src/constant/CommandType.ts @@ -0,0 +1,214 @@ +'use strict'; + +export enum CommandType { + COMM_INITIALIZATION = 'E', + COMM_GET_AES_KEY = 0x19, + COMM_RESPONSE = 'T', + + /** + * Add management + */ + COMM_ADD_ADMIN = 'V', + + /** + * Check the administrator + */ + COMM_CHECK_ADMIN = 'A', + + /** + * Administrator keyboard password + */ + COMM_SET_ADMIN_KEYBOARD_PWD = 'S', + + /** + * Delete password + */ + COMM_SET_DELETE_PWD = 'D', + + /** + * Set the lock name + */ + COMM_SET_LOCK_NAME = 'N', + + /** + * Sync keyboard password + */ + COMM_SYN_KEYBOARD_PWD = 'I', + + /** + * Verify user time + */ + COMM_CHECK_USER_TIME = 'U', + + /** + * Get the parking lock alarm record (the parking lock is moved) + * To determine the completion of operations such as adding and password + */ + COMM_GET_ALARM_ERRCORD_OR_OPERATION_FINISHED = 'W', + + /** + * Open the door + */ + COMM_UNLOCK = 'G', + + /** + * close the door + */ + COMM_LOCK = 'L', + + /** + * Calibration time + */ + COMM_TIME_CALIBRATE = 'C', + + /** + * Manage keyboard password + */ + COMM_MANAGE_KEYBOARD_PASSWORD = 0x03, + + /** + * Get a valid keyboard password in the lock + */ + COMM_GET_VALID_KEYBOARD_PASSWORD = 0x04, + + /** + * Get operation records + */ + COMM_GET_OPERATE_LOG = 0x25, + + /** + * Random number verification + */ + COMM_CHECK_RANDOM = 0x30, + + /** + * Three generations + * Password initialization + */ + COMM_INIT_PASSWORDS = 0x31, + + /** + * Read password parameters + */ + COMM_READ_PWD_PARA = 0x32, + + /** + * Modify the number of valid keyboard passwords + */ + COMM_RESET_KEYBOARD_PWD_COUNT = 0x33, + + /** + * Read door lock time + */ + COMM_GET_LOCK_TIME = 0x34, + + /** + * Reset lock + */ + COMM_RESET_LOCK = 'R', + + /** + * Query device characteristics + */ + COMM_SEARCHE_DEVICE_FEATURE = 0x01, + + /** + * IC card management + */ + COMM_IC_MANAGE = 0x05, + + /** + * Fingerprint management + */ + COMM_FR_MANAGE = 0x06, + + /** + * Get password list + */ + COMM_PWD_LIST = 0x07, + + /** + * Set the bracelet KEY + */ + COMM_SET_WRIST_BAND_KEY = 0x35, + + /** + * Automatic locking management (including door sensor) + */ + COMM_AUTO_LOCK_MANAGE = 0x36, + + /** + * Read device information + */ + COMM_READ_DEVICE_INFO = 0x90, + + /** + * Enter upgrade mode + */ + COMM_ENTER_DFU_MODE = 0x02, + + /** + * Query bicycle status (including door sensor) + */ + COMM_SEARCH_BICYCLE_STATUS = 0x14, + + /** + * Locked + */ + COMM_FUNCTION_LOCK = 0x58, + + /** + * The password is displayed on the screen + */ + COMM_SHOW_PASSWORD = 0x59, + + /** + * Control remote unlocking + */ + COMM_CONTROL_REMOTE_UNLOCK = 0x37, + + COMM_AUDIO_MANAGE = 0x62, + + COMM_REMOTE_CONTROL_DEVICE_MANAGE = 0x63, + + /** + * For NB networked door locks, through this command, App tells the address information of the door lock server + */ + COMM_CONFIGURE_NB_ADDRESS = 0x12, + + /** + * Hotel lock parameter configuration + */ + COMM_CONFIGURE_HOTEL_DATA = 0x64, + + /** + * Read the administrator password + */ + COMM_GET_ADMIN_CODE = 0x65, + + /** + * Normally open mode management + */ + COMM_CONFIGURE_PASSAGE_MODE = 0x66, + + /** + * Switch control instructions (privacy lock, tamper-proof alarm, reset lock) + */ + COMM_SWITCH = 0x68, + + COMM_FREEZE_LOCK = 0x61, + + COMM_LAMP = 0x67, + + /** + * Deadlock instruction + */ + COMM_DEAD_LOCK = 0x69, + + /** + * Cycle instructions + */ + COMM_CYCLIC_CMD = 0x70, + + COMM_NB_ACTIVATE_CONFIGURATION = 0x13, +} \ No newline at end of file diff --git a/src/constant/ConfigRemoteUnlock.ts b/src/constant/ConfigRemoteUnlock.ts new file mode 100644 index 0000000..cab2220 --- /dev/null +++ b/src/constant/ConfigRemoteUnlock.ts @@ -0,0 +1,8 @@ +'use strict'; + +export enum ConfigRemoteUnlock { + OP_TYPE_SEARCH = 1, + OP_TYPE_MODIFY = 2, + OP_CLOSE = 0, + OP_OPEN = 1, +} \ No newline at end of file diff --git a/src/constant/ControlAction.ts b/src/constant/ControlAction.ts new file mode 100644 index 0000000..07b8d46 --- /dev/null +++ b/src/constant/ControlAction.ts @@ -0,0 +1,17 @@ +'use strict'; + +export enum ControlAction { + UNLOCK = 3, + LOCK = 3 << 1, + /** + * Volume gate + */ + ROLLING_GATE_UP = 1, + ROLLING_GATE_DOWN = 1 << 1, + ROLLING_GATE_PAUSE = 1 << 2, + ROLLING_GATE_LOCK = 1 << 3, + /** + * + */ + HOLD = 3 << 3, +} \ No newline at end of file diff --git a/src/constant/CyclicOpType.ts b/src/constant/CyclicOpType.ts new file mode 100644 index 0000000..8dbd3cd --- /dev/null +++ b/src/constant/CyclicOpType.ts @@ -0,0 +1,15 @@ +'use strict'; + +export enum CyclicOpType { + CYCLIC_TYPE_WEEK = 1, + CYCLIC_TYPE_DAY = 2, + CYCLIC_MONTH_DAY = 3, + + QUERY = 1, + ADD = 2, + REMOVE = 3, + CLEAR = 4, + + USER_TYPE_FR = 1, + USER_TYPE_IC = 2, +} \ No newline at end of file diff --git a/src/constant/DateConstant.ts b/src/constant/DateConstant.ts new file mode 100644 index 0000000..e6c9f37 --- /dev/null +++ b/src/constant/DateConstant.ts @@ -0,0 +1,6 @@ +'use strict'; + +export enum DateConstant { + START_DATE_TIME = '200001010000', + END_DATE_TIME = '209912012359', +} \ No newline at end of file diff --git a/src/constant/DeviceInfoEnum.ts b/src/constant/DeviceInfoEnum.ts new file mode 100644 index 0000000..2402847 --- /dev/null +++ b/src/constant/DeviceInfoEnum.ts @@ -0,0 +1,53 @@ +'use strict'; + +export enum DeviceInfoEnum { + /** + * Product number + */ + MODEL_NUMBER = 1, + + /** + * Hardware version number + */ + HARDWARE_REVISION = 2, + + /** + * Firmware version number + */ + FIRMWARE_REVISION = 3, + + /** + * Production Date + */ + MANUFACTURE_DATE = 4, + + /** + * Bluetooth address + */ + MAC_ADDRESS = 5, + + /** + * Clock + */ + LOCK_CLOCK = 6, + + /** + * Operator information + */ + NB_OPERATOR = 7, + + /** + * NB module number (IMEI) + */ + NB_IMEI = 8, + + /** + * NB card information + */ + NB_CARD_INFO = 9, + + /** + * NB signal value + */ + NB_RSSI = 10, +} \ No newline at end of file diff --git a/src/constant/FeatureValue.ts b/src/constant/FeatureValue.ts new file mode 100644 index 0000000..966ab70 --- /dev/null +++ b/src/constant/FeatureValue.ts @@ -0,0 +1,160 @@ +'use strict'; + +export enum FeatureValue { + /** + * Password + */ + PASSCODE = 0, + + /** + * CARD + */ + IC = 1, + + /** + * Fingerprint + */ + FINGER_PRINT = 2, + + /** + * wristband + */ + WRIST_BAND = 3, + + /** + * Automatic locking function + */ + AUTO_LOCK = 4, + + /** + * Password with delete function + */ + PASSCODE_WITH_DELETE_FUNCTION = 5, + + /** + * Support firmware upgrade setting instructions + */ + FIRMWARE_SETTTING = 6, + + /** + * Modify password (custom) function + */ + MODIFY_PASSCODE_FUNCTION = 7, + + /** + * Blocking instruction + */ + MANUAL_LOCK = 8, + + /** + * Support password display or hide + */ + PASSWORD_DISPLAY_OR_HIDE = 9, + + /** + * Support gateway unlock command + */ + GATEWAY_UNLOCK = 10, + + /** + * Support gateway freeze and unfreeze instructions + */ + FREEZE_LOCK = 11, + + /** + * Support cycle password + */ + CYCLIC_PASSWORD = 12, + + /** + * Support door sensor + */ + MAGNETOMETER = 13, + + /** + * Support remote unlocking configuration + */ + CONFIG_GATEWAY_UNLOCK = 14, + + /** + * Audio management + */ + AUDIO_MANAGEMENT = 15, + + /** + * Support NB + */ + NB_LOCK = 16, + +// /** +// * Support hotel lock card system +// */ +// @Deprecated +// HOTEL_LOCK = 0x20000, + + /** + * Support reading the administrator password + */ + GET_ADMIN_CODE = 18, + + /** + * Support hotel lock card system + */ + HOTEL_LOCK = 19, + + /** + * Lock without clock chip + */ + LOCK_NO_CLOCK_CHIP = 20, + + /** + * Bluetooth does not broadcast, and it cannot be realized by clicking on the app to unlock + */ + CAN_NOT_CLICK_UNLOCK = 21, + + /** + * Support the normal open mode from a few hours to a few hours on a certain day + */ + PASSAGE_MODE = 22, + + /** + * In the case of supporting the normally open mode and setting the automatic lock, whether to support the closing of the automatic lock + */ + PASSAGE_MODE_AND_AUTO_LOCK_AND_CAN_CLOSE = 23, + + WIRELESS_KEYBOARD = 24, + + /** + * flashlight + */ + LAMP = 25, + + /** + * Anti-tamper switch configuration + */ + TAMPER_ALERT = 28, + + /** + * Reset key configuration + */ + RESET_BUTTON = 29, + + /** + * Anti-lock + */ + PRIVACK_LOCK = 30, + + /** + * Deadlock (the original 31 is not used) + */ + DEAD_LOCK = 32, + + /** + * Support normally open mode exception + */ +// PASSAGE_MODE_ = 33, + + CYCLIC_IC_OR_FINGER_PRINT = 34, + + NB_ACTIVITE_CONFIGURATION = 39, +} \ No newline at end of file diff --git a/src/constant/ICOperate.ts b/src/constant/ICOperate.ts new file mode 100644 index 0000000..dde6d06 --- /dev/null +++ b/src/constant/ICOperate.ts @@ -0,0 +1,20 @@ +'use strict'; + +export enum ICOperate { + IC_SEARCH = 1, + FR_SEARCH = 6, + ADD = 2, + DELETE = 3, + CLEAR = 4, + MODIFY = 5, + + /** + * Fingerprint template data package + */ + WRITE_FR = 7, + + STATUS_ADD_SUCCESS = 0x01, + STATUS_ENTER_ADD_MODE = 0x02, + STATUS_FR_PROGRESS = 0x03, + STATUS_FR_RECEIVE_TEMPLATE = 0x04, +} \ No newline at end of file diff --git a/src/constant/KeyboardPwdType.ts b/src/constant/KeyboardPwdType.ts new file mode 100644 index 0000000..8183dd7 --- /dev/null +++ b/src/constant/KeyboardPwdType.ts @@ -0,0 +1,23 @@ +'use strict'; + +export enum KeyboardPwdType { + /** + * Unlimited + */ + PWD_TYPE_PERMANENT = 1, + + /** + * Limited times + */ + PWD_TYPE_COUNT = 2, + + /** + * Limited time + */ + PWD_TYPE_PERIOD = 3, + + /** + * Loop + */ + PWD_TYPE_CIRCLE = 4, +} \ No newline at end of file diff --git a/src/constant/Lock.ts b/src/constant/Lock.ts new file mode 100644 index 0000000..ac8ac47 --- /dev/null +++ b/src/constant/Lock.ts @@ -0,0 +1,141 @@ +'use strict'; + +import { TTDevice } from "../device/TTDevice"; + +export enum LockType { + UNKNOWN = 0, + LOCK_TYPE_V1 = 1, + /** 3.0 */ + LOCK_TYPE_V2 = 2, + /** 5.1 */ + LOCK_TYPE_V2S = 3, + /** 5.4 */ + LOCK_TYPE_V2S_PLUS = 4, + /** Third generation lock 5.3 */ + LOCK_TYPE_V3 = 5, + /** Parking lock a.1 */ + LOCK_TYPE_CAR = 6, + /** Third generation parking lock 5.3.7 */ + LOCK_TYPE_V3_CAR = 8, + /** Electric car lock b.1 */ + LOCK_TYPE_MOBI = 7, + + // /** Remote control equipment 5.3.10 */ + // static LOCK_TYPE_REMOTE_CONTROL_DEVICE:number = 9; + // /** safe lock */ + // static LOCK_TYPE_SAFE_LOCK:number = 8; + // /** bicycle lock */ + // static LOCK_TYPE_BICYCLE:number = 9; +} + +export class LockVersion { + + static lockVersion_V2S_PLUS: LockVersion = new LockVersion(5, 4, 1, 1, 1); + static lockVersion_V3: LockVersion = new LockVersion(5, 3, 1, 1, 1); + static lockVersion_V2S: LockVersion = new LockVersion(5, 1, 1, 1, 1); + /** + *The second-generation parking lock scene is also changed to 7 + */ + static lockVersion_Va: LockVersion = new LockVersion(0x0a, 1, 0x07, 1, 1); + /** + *The electric car lock scene will be changed to 1 and there is no electric car lock + */ + static lockVersion_Vb: LockVersion = new LockVersion(0x0b, 1, 0x01, 1, 1); + static lockVersion_V2: LockVersion = new LockVersion(3, 0, 0, 0, 0); + static lockVersion_V3_car: LockVersion = new LockVersion(5, 3, 7, 1, 1); + + private protocolType: number; + private protocolVersion: number; + private scene: number; + private groupId: number; + private orgId: number; + + constructor(protocolType: number, protocolVersion: number, scene: number, groupId: number, orgId: number) { + this.protocolType = protocolType; + this.protocolVersion = protocolVersion; + this.scene = scene; + this.groupId = groupId; + this.orgId = orgId; + } + + getProtocolType(): number { + return this.protocolType; + } + setProtocolType(protocolType: number): void { + this.protocolType = protocolType; + } + getProtocolVersion(): number { + return this.protocolVersion; + } + setProtocolVersion(protocolVersion: number): void { + this.protocolVersion = protocolVersion; + } + getScene(): number { + return this.scene; + } + setScene(scene: number): void { + this.scene = scene; + } + getGroupId(): number { + return this.groupId; + } + setGroupId(groupId: number): void { + this.groupId = groupId; + } + getOrgId(): number { + return this.orgId; + } + setOrgId(orgId: number): void { + this.orgId = orgId; + } + + static getLockVersion(lockType: LockType): LockVersion | null { + switch (lockType) { + case LockType.LOCK_TYPE_V3_CAR: + return LockVersion.lockVersion_V3_car; + case LockType.LOCK_TYPE_V3: + return LockVersion.lockVersion_V3; + case LockType.LOCK_TYPE_V2S_PLUS: + return LockVersion.lockVersion_V2S_PLUS; + case LockType.LOCK_TYPE_V2S: + return LockVersion.lockVersion_V2S; + case LockType.LOCK_TYPE_CAR: + return LockVersion.lockVersion_Va; + case LockType.LOCK_TYPE_MOBI: + return LockVersion.lockVersion_Vb; + case LockType.LOCK_TYPE_V2: + return LockVersion.lockVersion_V2; + default: + return null; + } + } + + static getLockType(device: TTDevice): LockType { + if (device.lockType == LockType.UNKNOWN) { + if (device.protocolType == 5 && device.protocolVersion == 3 && device.scene == 7) { + device.lockType = LockType.LOCK_TYPE_V3_CAR; + } + else if (device.protocolType == 10 && device.protocolVersion == 1) { + device.lockType = LockType.LOCK_TYPE_CAR; + } + else if (device.protocolType == 11 && device.protocolVersion == 1) { + device.lockType = LockType.LOCK_TYPE_MOBI; + } + else if (device.protocolType == 5 && device.protocolVersion == 4) { + device.lockType = LockType.LOCK_TYPE_V2S_PLUS; + } + else if (device.protocolType == 5 && device.protocolVersion == 3) { + device.lockType = LockType.LOCK_TYPE_V3; + } + else if ((device.protocolType == 5 && device.protocolVersion == 1) || (device.name != null && device.name.toUpperCase().startsWith("LOCK_"))) { + device.lockType = LockType.LOCK_TYPE_V2S; + } + } + return device.lockType; + } + + toString(): string { + return this.protocolType + "," + this.protocolVersion + "," + this.scene + "," + this.groupId + "," + this.orgId; + } + +} diff --git a/src/constant/LockedStatus.ts b/src/constant/LockedStatus.ts new file mode 100644 index 0000000..4cb7d18 --- /dev/null +++ b/src/constant/LockedStatus.ts @@ -0,0 +1,7 @@ +'use strict'; + +export enum LockedStatus { + UNKNOWN = -1, + LOCKED = 0, + UNLOCKED = 1 +} \ No newline at end of file diff --git a/src/constant/OperationType.ts b/src/constant/OperationType.ts new file mode 100644 index 0000000..b420029 --- /dev/null +++ b/src/constant/OperationType.ts @@ -0,0 +1,8 @@ +'use strict'; + +export enum OperationType { + //Inquire + GET_STATE = 1, + //modify + MODIFY = 2, +} \ No newline at end of file diff --git a/src/constant/PassageModeOperate.ts b/src/constant/PassageModeOperate.ts new file mode 100644 index 0000000..6337767 --- /dev/null +++ b/src/constant/PassageModeOperate.ts @@ -0,0 +1,8 @@ +'use strict'; + +export enum PassageModeOperate { + QUERY = 1, + ADD = 2, + DELETE = 3, + CLEAR = 4, +} \ No newline at end of file diff --git a/src/constant/PassageModeType.ts b/src/constant/PassageModeType.ts new file mode 100644 index 0000000..f7dfb36 --- /dev/null +++ b/src/constant/PassageModeType.ts @@ -0,0 +1,6 @@ +'use strict'; + +export enum PassageModeType { + WEEKLY = 1, + MONTHLY = 2 +} \ No newline at end of file diff --git a/src/constant/PwdOperateType.ts b/src/constant/PwdOperateType.ts new file mode 100644 index 0000000..f336025 --- /dev/null +++ b/src/constant/PwdOperateType.ts @@ -0,0 +1,28 @@ +'use strict'; + +export enum PwdOperateType { + /** + * Clear keyboard password + */ + PWD_OPERATE_TYPE_CLEAR = 1, + + /** + * Add keyboard password + */ + PWD_OPERATE_TYPE_ADD = 2, + + /** + * Delete a single keyboard password + */ + PWD_OPERATE_TYPE_REMOVE_ONE = 3, + + /** + * Change the keyboard password (the old one is 4, no longer used) + */ + PWD_OPERATE_TYPE_MODIFY = 5, + + /** + * Recovery password + */ + PWD_OPERATE_TYPE_RECOVERY = 6, +} \ No newline at end of file diff --git a/src/device/AdminType.ts b/src/device/AdminType.ts new file mode 100644 index 0000000..5529922 --- /dev/null +++ b/src/device/AdminType.ts @@ -0,0 +1,6 @@ +'use strict'; + +export type AdminType = { + adminPs: number; + unlockKey: number; +} \ No newline at end of file diff --git a/src/device/DeviceInfoType.ts b/src/device/DeviceInfoType.ts new file mode 100644 index 0000000..25567d7 --- /dev/null +++ b/src/device/DeviceInfoType.ts @@ -0,0 +1,47 @@ +'use strict'; + +export type DeviceInfoType = { + /** + * hex feature + */ + featureValue: string; + + /** + * Product model(e.g. "M201") + */ + modelNum: string; + /** + * Hardware version(e.g. "1.3") + */ + hardwareRevision: string; + /** + * Firmware version(e.g. "2.1.16.705") + */ + firmwareRevision: string; + /** + * NB lock IMEI + */ + nbNodeId: string; + + /** + * NB operator + */ + nbOperator: string; + + /** + * NB lock card info + */ + nbCardNumber: string; + /** + * NB lock rssi + */ + nbRssi: number; + /** + * Date of manufacture(e.g. "20160707") + */ + factoryDate: string; + /** + * lock clock(e.g. "1701051531") yymmddhhmm + */ + lockClock: string; +} diff --git a/src/device/PrivateDataType.ts b/src/device/PrivateDataType.ts new file mode 100644 index 0000000..307f976 --- /dev/null +++ b/src/device/PrivateDataType.ts @@ -0,0 +1,11 @@ +'use strict'; + +import { CodeSecret } from "../api/Commands/InitPasswordsCommand"; +import { AdminType } from "./AdminType"; + +export type PrivateDataType = { + aesKey?: Buffer; + admin?: AdminType; + adminPasscode?: string; + pwdInfo?: CodeSecret[]; +} \ No newline at end of file diff --git a/src/device/TTBluetoothDevice.ts b/src/device/TTBluetoothDevice.ts new file mode 100644 index 0000000..07d1393 --- /dev/null +++ b/src/device/TTBluetoothDevice.ts @@ -0,0 +1,356 @@ +'use strict'; + +import { CommandEnvelope } from "../api/CommandEnvelope"; +import { LockType, LockVersion } from "../constant/Lock"; +import { CharacteristicInterface, DeviceInterface, ServiceInterface } from "../scanner/DeviceInterface"; +import { sleep } from "../util/timingUtil"; +import { TTDevice } from "./TTDevice"; + +const CRLF = "0d0a"; +const MTU = 20; + +export interface TTBluetoothDevice { + on(event: "connected", listener: () => void): this; + on(event: "disconnected", listener: () => void): this; + on(event: "dataReceived", listener: (command: CommandEnvelope) => void): this; +} + +export class TTBluetoothDevice extends TTDevice implements TTBluetoothDevice { + device?: DeviceInterface; + connected: boolean = false; + incomingDataBuffer: Buffer = Buffer.from([]); + private waitingForResponse: boolean = false; + private responses: CommandEnvelope[] = []; + + private constructor() { + super(); + } + + static createFromDevice(device: DeviceInterface): TTBluetoothDevice { + const bDevice = new TTBluetoothDevice(); + bDevice.id = device.id; + bDevice.updateFromDevice(device); + return bDevice; + } + + updateFromDevice(device: DeviceInterface): boolean { + // just check if we are updating the same device + if (this.id == device.id) { + this.device = device; + this.name = device.name; + this.rssi = device.rssi; + if (device.manufacturerData.length >= 15) { + this.parseManufacturerData(device.manufacturerData); + } + this.device.on("connected", this.onDeviceConnected.bind(this)); + this.device.on("disconnected", this.onDeviceDisconnected.bind(this)); + return true; + } + return false; + } + + async connect(): Promise { + if (this.device && this.device.connectable) { + if (await this.device.connect()) { + await this.readBasicInfo(); + await this.subscribe(); + this.connected = true; + this.emit("connected"); + return true; + } + } + return false; + } + + private async onDeviceConnected() { + // await this.readBasicInfo(); + // await this.subscribe(); + // this.connected = true; + // this.emit("connected"); + } + + private async onDeviceDisconnected() { + this.connected = false; + this.emit("disconnected"); + } + + private async readBasicInfo() { + await this.device?.discoverServices(); + // update some basic information + let service = this.device?.services.get("1800"); + if (typeof service != "undefined") { + await service.readCharacteristics(); + this.putCharacteristicValue(service, "2a00", "name"); + } + service = this.device?.services.get("180a"); + if (typeof service != "undefined") { + await service.readCharacteristics(); + this.putCharacteristicValue(service, "2a29", "manufacturer"); + this.putCharacteristicValue(service, "2a24", "model"); + this.putCharacteristicValue(service, "2a27", "hardware"); + this.putCharacteristicValue(service, "2a26", "firmware"); + } + } + + private async subscribe() { + let service = this.device?.services.get("1910"); + if (typeof service != "undefined") { + const characteristic = service.characteristics.get("fff4"); + if (typeof characteristic != "undefined") { + await characteristic.subscribe(); + characteristic.on("dataRead", this.onIncomingData.bind(this)); + await characteristic.discoverDescriptors(); + const descriptor = characteristic.descriptors.get("2902"); + if (typeof descriptor != "undefined") { + console.log("Subscribing to descriptor notifications"); + await descriptor.writeValue(Buffer.from([0x01, 0x00])); // BE + // await descriptor.writeValue(Buffer.from([0x00, 0x01])); // LE + } + } + } + } + + async sendCommand(command: CommandEnvelope, waitForResponse: boolean = true, ignoreCrc: boolean = false): Promise { + if (this.waitingForResponse) { + throw new Error("Command already in progress"); + } + if (this.responses.length > 0) { + // should this be an error ? + throw new Error("Unprocessed responses"); + } + const commandData = command.buildCommandBuffer(); + if (commandData) { + let data = Buffer.concat([ + commandData, + Buffer.from(CRLF, "hex") + ]); + // write with 20 bytes MTU + const service = this.device?.services.get("1910"); + if (typeof service != undefined) { + const characteristic = service?.characteristics.get("fff2"); + if (typeof characteristic != "undefined") { + if (waitForResponse) { + let retry = 0; + let response: CommandEnvelope | undefined; + this.waitingForResponse = true; + do { + if (retry > 0) { + // wait a bit before retry + console.log("Sleeping a bit"); + await sleep(500); + } + await this.writeCharacteristic(characteristic, data); + // wait for a response + console.log("Waiting for response"); + let cycles = 0; + while (this.responses.length == 0) { + cycles++; + await sleep(5); + } + console.log("Waited for a response for", cycles, "=", cycles * 5, "ms"); + response = this.responses.pop(); + retry++; + } while (typeof response == "undefined" || (!response.isCrcOk() && !ignoreCrc && retry <= 2)); + this.waitingForResponse = false; + if (!response.isCrcOk() && !ignoreCrc) { + throw new Error("Malformed response, bad CRC"); + } + return response; + } else { + await this.writeCharacteristic(characteristic, data); + } + } + } + } + } + + /** + * + * @param timeout Timeout to wait in ms + */ + async waitForResponse(timeout: number = 10000): Promise { + if (this.waitingForResponse) { + throw new Error("Command already in progress"); + } + let response: CommandEnvelope | undefined; + this.waitingForResponse = true; + + console.log("Waiting for response"); + let cycles = 0; + const sleepPerCycle = 100; + while (this.responses.length == 0 && cycles * sleepPerCycle < timeout) { + cycles++; + await sleep(sleepPerCycle); + } + console.log("Waited for a response for", cycles, "=", cycles * sleepPerCycle, "ms"); + + if (this.responses.length > 0) { + response = this.responses.pop(); + } + this.waitingForResponse = false; + return response; + } + + private async writeCharacteristic(characteristic: CharacteristicInterface, data: Buffer): Promise { + console.log("Sending command:", data.toString("hex")); + let index = 0; + do { + const remaining = data.length - index; + await characteristic.write(data.subarray(index, index + Math.min(MTU, remaining)), true); + // await sleep(10); + index += MTU; + } while (index < data.length); + } + + private onIncomingData(data: Buffer) { + this.incomingDataBuffer = Buffer.concat([this.incomingDataBuffer, data]); + this.readDeviceResponse(); + } + + private readDeviceResponse() { + if (this.incomingDataBuffer.length >= 2) { + // check for CRLF at the end of data + const ending = this.incomingDataBuffer.subarray(this.incomingDataBuffer.length - 2); + if (ending.toString("hex") == CRLF) { + // we have a command response + console.log("Received response:", this.incomingDataBuffer.toString("hex")); + try { + const command = CommandEnvelope.createFromRawData(this.incomingDataBuffer.subarray(0, this.incomingDataBuffer.length - 2)); + if (this.waitingForResponse) { + this.responses.push(command); + } else { + // discard unsolicited messages if CRC is not ok + if (command.isCrcOk()) { + this.emit("dataReceived", command); + } + } + } catch (error) { + // TODO: in case of a malformed response we should notify the waiting cycle and stop waiting + console.error(error); + } + this.incomingDataBuffer = Buffer.from([]); + } + } + } + + private putCharacteristicValue(service: ServiceInterface, uuid: string, property: string) { + const value = service.characteristics.get(uuid); + if (typeof value != "undefined" && typeof value.lastValue != "undefined") { + Reflect.set(this, property, value.lastValue.toString()); + } + } + + async disconnect() { + if (await this.device?.disconnect()) { + this.connected = false; + } + } + + parseManufacturerData(manufacturerData: Buffer) { + // TODO: check offset is within the limits of the Buffer + // console.log(manufacturerData, manufacturerData.length) + if (manufacturerData.length < 15) { + throw new Error("Invalid manufacturer data length:" + manufacturerData.length.toString()); + } + var offset = 0; + this.protocolType = manufacturerData.readInt8(offset++); + this.protocolVersion = manufacturerData.readInt8(offset++); + if (this.protocolType == 18 && this.protocolVersion == 25) { + this.isDfuMode = true; + return; + } + if (this.protocolType == -1 && this.protocolVersion == -1) { + this.isDfuMode = true; + return; + } + if (this.protocolType == 52 && this.protocolVersion == 18) { + this.isWristband = true; + } + if (this.protocolType == 5 && this.protocolVersion == 3) { + this.scene = manufacturerData.readInt8(offset++); + } else { + offset = 4; + this.protocolType = manufacturerData.readInt8(offset++); + this.protocolVersion = manufacturerData.readInt8(offset++); + offset = 7; + this.scene = manufacturerData.readInt8(offset++); + } + if (this.protocolType < 5 || LockVersion.getLockType(this) == LockType.LOCK_TYPE_V2S) { + this.isRoomLock = true; + return; + } + if (this.scene <= 3) { + this.isRoomLock = true; + } else { + switch (this.scene) { + case 4: { + this.isGlassLock = true; + break; + } + case 5: + case 11: { + this.isSafeLock = true; + break; + } + case 6: { + this.isBicycleLock = true; + break; + } + case 7: { + this.isLockcar = true; + break; + } + case 8: { + this.isPadLock = true; + break; + } + case 9: { + this.isCyLinder = true; + break; + } + case 10: { + if (this.protocolType == 5 && this.protocolVersion == 3) { + this.isRemoteControlDevice = true; + break; + } + break; + } + } + } + const params = manufacturerData.readInt8(offset); + this.isUnlock = ((params & 0x1) == 0x1); + this.isSettingMode = ((params & 0x4) != 0x0); + if (LockVersion.getLockType(this) == LockType.LOCK_TYPE_V3 || LockVersion.getLockType(this) == LockType.LOCK_TYPE_V3_CAR) { + this.isTouch = ((params && 0x8) != 0x0); + } else if (LockVersion.getLockType(this) == LockType.LOCK_TYPE_CAR) { + this.isTouch = false; + this.isLockcar = true; + } + if (this.isLockcar) { + if (this.isUnlock) { + if ((params & 0x10) == 0x1) { + this.parkStatus = 3; + } else { + this.parkStatus = 2; + } + } else if ((params & 0x10) == 0x1) { + this.parkStatus = 1; + } else { + this.parkStatus = 0; + } + } + offset++; + this.batteryCapacity = manufacturerData.readInt8(offset); + // offset += 3 + 4; // Offset in original SDK is + 3, but in scans it's actually +4 + offset = manufacturerData.length - 6; // let's just get the last 6 bytes + const macBuf = manufacturerData.slice(offset, offset + 6); + var macArr: string[] = []; + macBuf.forEach((m: number) => { + macArr.push(m.toString(16)); + }); + macArr.reverse(); + this.address = macArr.join(':').toUpperCase(); + } + + +} \ No newline at end of file diff --git a/src/device/TTDevice.ts b/src/device/TTDevice.ts new file mode 100644 index 0000000..d99cfb4 --- /dev/null +++ b/src/device/TTDevice.ts @@ -0,0 +1,83 @@ +'use strict'; + +import { EventEmitter } from "events"; +import { LockType } from "../constant/Lock"; + +export class TTDevice extends EventEmitter { + // public data + id: string = ""; + uuid: string = ""; + name: string = ""; + manufacturer: string = "unknown"; + model: string = "unknown"; + hardware: string = "unknown"; + firmware: string = "unknown"; + address: string = ""; + rssi: number = 0; + /** @type {byte} */ + protocolType: number = 0; + /** @type {byte} */ + protocolVersion: number = 0; + /** @type {byte} */ + scene: number = 0; + /** @type {byte} */ + groupId: number = 0; + /** @type {byte} */ + orgId: number = 0; + /** @type {byte} */ + lockType: LockType = LockType.UNKNOWN; + isTouch: boolean = false; + isSettingMode: boolean = false; + isUnlock: boolean = false; + /** @type {byte} */ + txPowerLevel: number = 0; + /** @type {byte} */ + batteryCapacity: number = -1; + /** @type {number} */ + date: number = 0; + isWristband: boolean = false; + isRoomLock: boolean = false; + isSafeLock: boolean = false; + isBicycleLock: boolean = false; + isLockcar: boolean = false; + isGlassLock: boolean = false; + isPadLock: boolean = false; + isCyLinder: boolean = false; + isRemoteControlDevice: boolean = false; + isDfuMode: boolean = false; + isNoLockService: boolean = false; + remoteUnlockSwitch: number = 0; + disconnectStatus: number = 0; + parkStatus: number = 0; + + toJSON(asObject:boolean = false): string | Object { + const temp = new TTDevice(); + var json = {}; + + // exclude keys that we don't need from the export + const excludedKeys = new Set([ + "_eventsCount" + ]); + + Object.getOwnPropertyNames(temp).forEach((key) => { + if (!excludedKeys.has(key)) { + const val = Reflect.get(this, key); + if (typeof val != 'undefined' && ((typeof val == "string" && val != "") || typeof val != "string")) { + if ((typeof val) == "object") { + if (val.length && val.length > 0) { + Reflect.set(json, key, val.toString('hex')); + } + } else { + Reflect.set(json, key, val); + } + } + } + }); + + if (asObject) { + return json; + } else { + return JSON.stringify(json); + } + } +} \ No newline at end of file diff --git a/src/device/TTLock.ts b/src/device/TTLock.ts new file mode 100644 index 0000000..4229e8f --- /dev/null +++ b/src/device/TTLock.ts @@ -0,0 +1,1115 @@ +'use strict'; + +import { CommandEnvelope } from "../api/CommandEnvelope"; +import { Fingerprint, ICCard, KeyboardPassCode, PassageModeData } from "../api/Commands"; +import { CodeSecret } from "../api/Commands/InitPasswordsCommand"; +import { AudioManage } from "../constant/AudioManage"; +import { ConfigRemoteUnlock } from "../constant/ConfigRemoteUnlock"; +import { FeatureValue } from "../constant/FeatureValue"; +import { KeyboardPwdType } from "../constant/KeyboardPwdType"; +import { LockType } from "../constant/Lock"; +import { LockedStatus } from "../constant/LockedStatus"; +import { PassageModeOperate } from "../constant/PassageModeOperate"; +import { TTLockData, TTLockPrivateData } from "../store/TTLockData"; +import { sleep } from "../util/timingUtil"; +import { TTBluetoothDevice } from "./TTBluetoothDevice"; +import { TTLockApi } from "./TTLockApi"; + +export interface TTLock { + on(event: "lockUpdated", listener: (lock: TTLock) => void): this; + on(event: "lockReset", listener: (address: string) => void): this; + on(event: "connected", listener: (lock: TTLock) => void): this; + on(event: "disconnected", listener: (lock: TTLock) => void): this; + on(event: "locked", listener: (lock: TTLock) => void): this; + on(event: "unlocked", listener: (lock: TTLock) => void): this; + /** Emited when an IC Card is ready to be scanned */ + on(event: "scanICStart", listener: (lock: TTLock) => void): this; + /** Emited when a fingerprint is ready to be scanned */ + on(event: "scanFRStart", listener: (lock: TTLock) => void): this; + /** Emited after each fingerprint scan */ + on(event: "scanFRProgress", listener: (lock: TTLock) => void): this; +} + +export class TTLock extends TTLockApi implements TTLock { + private connected: boolean; + private skipDataRead: boolean = false; + + constructor(device: TTBluetoothDevice, data?: TTLockData) { + super(device, data); + this.connected = false; + + this.device.on("dataReceived", this.onDataReceived.bind(this)); + this.device.on("connected", this.onConnected.bind(this)); + this.device.on("disconnected", this.onDisconnected.bind(this)); + } + + getAddress(): string { + return this.device.address; + } + + getName(): string { + return this.device.name; + } + + getManufacturer(): string { + return this.device.manufacturer; + } + + getModel(): string { + return this.device.model; + } + + getFirmware(): string { + return this.device.firmware; + } + + getBattery(): number { + return this.batteryCapacity; + } + + getRssi(): number { + return this.rssi; + } + + async connect(skipDataRead: boolean = false): Promise { + this.skipDataRead = skipDataRead; + const connected = await this.device.connect(); + if (connected) { + do { + await sleep(500); + } while (!this.connected); + } + this.skipDataRead = false; + return connected; + } + + isConnected(): boolean { + return this.connected; + } + + async disconnect(): Promise { + await this.device.disconnect(); + } + + isInitialized(): boolean { + return this.initialized; + } + + isPaired(): boolean { + const privateData = this.privateData; + if (privateData.aesKey && privateData.admin && privateData.admin.adminPs && privateData.admin.unlockKey) { + return true; + } else { + return false; + } + } + + /** + * Initialize and pair with a new lock + */ + async initLock(): Promise { + if (!this.isConnected()) { + throw new Error("Lock is not connected"); + } + + if (this.initialized) { + throw new Error("Lock is not in pairing mode"); + } + + // TODO: also check if lock is already inited (has AES key) + + try { + // Init + console.log("========= init"); + await this.initCommand(); + console.log("========= init"); + + // Get AES key + console.log("========= AES key"); + const aesKey = await this.getAESKeyCommand(); + console.log("========= AES key:", aesKey.toString("hex")); + + // Add admin + console.log("========= admin"); + const admin = await this.addAdminCommand(aesKey); + console.log("========= admin:", admin); + + // Calibrate time + console.log("========= time"); + await this.calibrateTimeCommand(aesKey); + console.log("========= time"); + + // Search device features + console.log("========= feature list"); + const featureList = await this.searchDeviceFeatureCommand(aesKey); + console.log("========= feature list", featureList); + + let switchState: any, + lockSound: AudioManage.TURN_ON | AudioManage.TURN_OFF | undefined, + displayPasscode: 0 | 1 | undefined, + autoLockTime: number | undefined, + lightingTime: number | undefined, + adminPasscode: string | undefined, + pwdInfo: CodeSecret[] | undefined, + remoteUnlock: ConfigRemoteUnlock.OP_OPEN | ConfigRemoteUnlock.OP_CLOSE | undefined; + + // Feature depended queries + if (featureList.has(FeatureValue.RESET_BUTTON) + || featureList.has(FeatureValue.TAMPER_ALERT) + || featureList.has(FeatureValue.PRIVACK_LOCK)) { + console.log("========= switchState"); + switchState = await this.getSwitchStateCommand(undefined, aesKey); + console.log("========= switchState:", switchState); + } + if (featureList.has(FeatureValue.AUDIO_MANAGEMENT)) { + console.log("========= lockSound"); + try { + lockSound = await this.audioManageCommand(undefined, aesKey); + } catch (error) { + // this sometimes fails + console.error(error); + } + console.log("========= lockSound:", lockSound); + } + if (featureList.has(FeatureValue.PASSWORD_DISPLAY_OR_HIDE)) { + console.log("========= displayPasscode"); + displayPasscode = await this.screenPasscodeManageCommand(undefined, aesKey); + console.log("========= displayPasscode:", displayPasscode); + } + if (featureList.has(FeatureValue.AUTO_LOCK)) { + console.log("========= autoLockTime"); + autoLockTime = await this.searchAutoLockTimeCommand(undefined, aesKey); + console.log("========= autoLockTime:", autoLockTime); + } + if (featureList.has(FeatureValue.LAMP)) { + console.log("========= lightingTime"); + lightingTime = await this.controlLampCommand(undefined, aesKey); + console.log("========= lightingTime:", lightingTime); + } + if (featureList.has(FeatureValue.GET_ADMIN_CODE)) { + // Command.COMM_GET_ADMIN_CODE + console.log("========= getAdminCode"); + adminPasscode = await this.getAdminCodeCommand(aesKey); + console.log("========= getAdminCode", adminPasscode); + if (adminPasscode == "") { + console.log("========= set adminPasscode"); + adminPasscode = await this.setAdminKeyboardPwdCommand(undefined, aesKey); + console.log("========= set adminPasscode:", adminPasscode); + } + } else if (this.device.lockType == LockType.LOCK_TYPE_V3_CAR) { + // Command.COMM_GET_ALARM_ERRCORD_OR_OPERATION_FINISHED + } else if (this.device.lockType == LockType.LOCK_TYPE_V3) { + console.log("========= set adminPasscode"); + adminPasscode = await this.setAdminKeyboardPwdCommand(undefined, aesKey); + console.log("========= set adminPasscode:", adminPasscode); + } + + console.log("========= init passwords"); + pwdInfo = await this.initPasswordsCommand(aesKey); + console.log("========= init passwords:", pwdInfo); + + if (featureList.has(FeatureValue.CONFIG_GATEWAY_UNLOCK)) { + console.log("========= remoteUnlock"); + remoteUnlock = await this.controlRemoteUnlockCommand(ConfigRemoteUnlock.OP_CLOSE, aesKey); + console.log("========= remoteUnlock:", remoteUnlock); + } + + console.log("========= finished"); + await this.operateFinishedCommand(aesKey); + console.log("========= finished"); + + // save all the data we gathered during init sequence + if (aesKey) this.privateData.aesKey = Buffer.from(aesKey); + if (admin) this.privateData.admin = admin; + if (featureList) this.featureList = featureList; + if (switchState) this.switchState = switchState; + if (lockSound) this.lockSound = lockSound; + if (displayPasscode) this.displayPasscode = displayPasscode; + if (autoLockTime) this.autoLockTime = autoLockTime; + if (lightingTime) this.lightingTime = lightingTime; + if (adminPasscode) this.privateData.adminPasscode = adminPasscode; + if (pwdInfo) this.privateData.pwdInfo = pwdInfo; + if (remoteUnlock) this.remoteUnlock = remoteUnlock; + this.lockedStatus = LockedStatus.LOCKED; // always locked by default + + // read device information + console.log("========= device info"); + try { + this.deviceInfo = await this.macro_readAllDeviceInfo(aesKey); + } catch (error) { + // this sometimes fails + console.error(error); + } + console.log("========= device info:", this.deviceInfo); + + } catch (error) { + console.error("Error while initialising lock", error); + return false; + } + + // TODO: we should now refresh the device's data (disconnect and reconnect maybe ?) + this.initialized = true; + this.emit("lockUpdated", this); + return true; + } + + /** + * Lock the lock + */ + async lock(): Promise { + if (!this.isConnected()) { + throw new Error("Lock is not connected"); + } + + if (!this.initialized) { + throw new Error("Lock is in pairing mode"); + } + + try { + console.log("========= check user time"); + const psFromLock = await this.checkUserTime(); + console.log("========= check user time", psFromLock); + console.log("========= lock"); + const lockData = await this.lockCommand(psFromLock); + console.log("========= lock", lockData); + this.lockedStatus = LockedStatus.LOCKED; + this.emit("locked", this); + } catch (error) { + console.error("Error locking the lock", error); + return false; + } + + return true; + } + + /** + * Unlock the lock + */ + async unlock(): Promise { + if (!this.isConnected()) { + throw new Error("Lock is not connected"); + } + + if (!this.initialized) { + throw new Error("Lock is in pairing mode"); + } + + try { + console.log("========= check user time"); + const psFromLock = await this.checkUserTime(); + console.log("========= check user time", psFromLock); + console.log("========= unlock"); + const unlockData = await this.unlockCommand(psFromLock); + console.log("========= unlock", unlockData); + this.lockedStatus = LockedStatus.UNLOCKED; + this.emit("unlocked", this); + // if autolock is on, then emit locked event after the timeout has passed + if (this.autoLockTime > 0) { + setTimeout(() => { + this.lockedStatus = LockedStatus.LOCKED; + this.emit("locked", this); + }, this.autoLockTime * 1000); + } + } catch (error) { + console.error("Error unlocking the lock", error); + return false; + } + + return true; + } + + /** + * Get the status of the lock (locked or unlocked) + */ + async getLockStatus(noCache: boolean = false): Promise { + if (!this.initialized) { + throw new Error("Lock is in pairing mode"); + } + + const oldStatus = this.lockedStatus; + + if (noCache || this.lockedStatus == LockedStatus.UNKNOWN) { + if (!this.isConnected()) { + throw new Error("Lock is not connected"); + } + + try { + console.log("========= check lock status"); + this.lockedStatus = await this.searchBycicleStatusCommand(); + console.log("========= check lock status", this.lockedStatus); + } catch (error) { + console.error("Error getting lock status", error); + } + + } + + if (oldStatus != this.lockedStatus) { + if (this.lockedStatus == LockedStatus.LOCKED) { + this.emit("locked", this); + } else { + this.emit("unlocked", this); + } + } + + return this.lockedStatus; + } + + async getAutolockTime(noCache: boolean = false): Promise { + if (!this.initialized) { + throw new Error("Lock is in pairing mode"); + } + + const oldAutoLockTime = this.autoLockTime; + + if (noCache || this.autoLockTime == -1) { + if (typeof this.featureList != "undefined") { + if (this.featureList.has(FeatureValue.AUTO_LOCK)) { + if (!this.isConnected()) { + throw new Error("Lock is not connected"); + } + + try { + if (await this.macro_adminLogin()) { + console.log("========= autoLockTime"); + this.autoLockTime = await this.searchAutoLockTimeCommand(); + console.log("========= autoLockTime:", this.autoLockTime); + } + } catch (error) { + console.error(error); + } + } + } + } + + if (oldAutoLockTime != this.autoLockTime) { + this.emit("lockUpdated", this); + } + + return this.autoLockTime; + } + + async setAutoLockTime(autoLockTime: number): Promise { + if (!this.isConnected()) { + throw new Error("Lock is not connected"); + } + + if (!this.initialized) { + throw new Error("Lock is in pairing mode"); + } + + if (this.autoLockTime != autoLockTime) { + if (typeof this.featureList != "undefined") { + if (this.featureList.has(FeatureValue.AUTO_LOCK)) { + try { + if (await this.macro_adminLogin()) { + console.log("========= autoLockTime"); + await this.searchAutoLockTimeCommand(autoLockTime); + console.log("========= autoLockTime"); + this.autoLockTime = autoLockTime; + return true; + } + } catch (error) { + console.error(error); + } + } + } + } + + return false; + } + + async resetLock(): Promise { + if (!this.isConnected()) { + throw new Error("Lock is not connected"); + } + + if (!this.initialized) { + throw new Error("Lock is in pairing mode"); + } + + try { + if (await this.macro_adminLogin()) { + console.log("========= reset"); + await this.resetLockCommand(); + console.log("========= reset"); + } else { + return false; + } + } catch (error) { + console.error("Error while reseting the lock", error); + return false; + } + + // TODO: disconnect, cleanup etc. + + this.emit("lockReset", this.device.address); + return true; + } + + async getPassageMode(): Promise { + if (!this.isConnected()) { + throw new Error("Lock is not connected"); + } + + if (!this.initialized) { + throw new Error("Lock is in pairing mode"); + } + + let data: PassageModeData[] = []; + + try { + if (await this.macro_adminLogin()) { + let sequence = 0; + do { + console.log("========= get passage mode"); + const response = await this.getPassageModeCommand(sequence); + console.log("========= get passage mode", response); + sequence = response.sequence; + response.data.forEach((passageData) => { + data.push(passageData); + }); + } while (sequence != -1); + } + } catch (error) { + console.error("Error while getting passage mode", error); + } + + return data; + } + + async setPassageMode(data: PassageModeData): Promise { + if (!this.isConnected()) { + throw new Error("Lock is not connected"); + } + + if (!this.initialized) { + throw new Error("Lock is in pairing mode"); + } + + try { + if (await this.macro_adminLogin()) { + console.log("========= set passage mode"); + await this.setPassageModeCommand(data); + console.log("========= set passage mode"); + } else { + return false; + } + } catch (error) { + console.error("Error while getting passage mode", error); + return false; + } + + return true; + } + + async deletePassageMode(data: PassageModeData): Promise { + if (!this.isConnected()) { + throw new Error("Lock is not connected"); + } + + if (!this.initialized) { + throw new Error("Lock is in pairing mode"); + } + + try { + if (await this.macro_adminLogin()) { + console.log("========= delete passage mode"); + await this.setPassageModeCommand(data, PassageModeOperate.DELETE); + console.log("========= delete passage mode"); + } + } catch (error) { + console.error("Error while deleting passage mode", error); + return false; + } + + return true; + } + + async clearPassageMode(): Promise { + if (!this.isConnected()) { + throw new Error("Lock is not connected"); + } + + if (!this.initialized) { + throw new Error("Lock is in pairing mode"); + } + + try { + if (await this.macro_adminLogin()) { + console.log("========= clear passage mode"); + await this.clearPassageModeCommand(); + console.log("========= clear passage mode"); + } else { + return false; + } + } catch (error) { + console.error("Error while deleting passage mode", error); + return false; + } + + return true; + } + + /** + * Add a new passcode to unlock + * @param type PassCode type: 1 - permanent, 2 - one time, 3 - limited time + * @param passCode 4-9 digits code + * @param startDate Valid from YYYYMMDDHHmm + * @param endDate Valid to YYYYMMDDHHmm + */ + async addPassCode(type: KeyboardPwdType, passCode: string, startDate?: string, endDate?: string): Promise { + if (!this.isConnected()) { + throw new Error("Lock is not connected"); + } + + if (!this.initialized) { + throw new Error("Lock is in pairing mode"); + } + + try { + if (await this.macro_adminLogin()) { + console.log("========= add passCode"); + const result = await this.createCustomPasscodeCommand(type, passCode, startDate, endDate); + console.log("========= add passCode", result); + return result; + } else { + return false; + } + } catch (error) { + console.error("Error while adding passcode", error); + return false; + } + } + + /** + * Update a passcode to unlock + * @param type PassCode type: 1 - permanent, 2 - one time, 3 - limited time + * @param oldPassCode 4-9 digits code - old code + * @param newPassCode 4-9 digits code - new code + * @param startDate Valid from YYYYMMDDHHmm + * @param endDate Valid to YYYYMMDDHHmm + */ + async updatePassCode(type: KeyboardPwdType, oldPassCode: string, newPassCode: string, startDate?: string, endDate?: string): Promise { + if (!this.isConnected()) { + throw new Error("Lock is not connected"); + } + + if (!this.initialized) { + throw new Error("Lock is in pairing mode"); + } + + try { + if (await this.macro_adminLogin()) { + console.log("========= update passCode"); + const result = await this.updateCustomPasscodeCommand(type, oldPassCode, newPassCode, startDate, endDate); + console.log("========= update passCode", result); + return result; + } else { + return false; + } + } catch (error) { + console.error("Error while updating passcode", error); + return false; + } + } + + /** + * Delete a set passcode + * @param type PassCode type: 1 - permanent, 2 - one time, 3 - limited time + * @param passCode 4-9 digits code + */ + async deletePassCode(type: KeyboardPwdType, passCode: string): Promise { + if (!this.isConnected()) { + throw new Error("Lock is not connected"); + } + + if (!this.initialized) { + throw new Error("Lock is in pairing mode"); + } + + try { + if (await this.macro_adminLogin()) { + console.log("========= delete passCode"); + const result = await this.deleteCustomPasscodeCommand(type, passCode); + console.log("========= delete passCode", result); + return result; + } else { + return false; + } + } catch (error) { + console.error("Error while deleting passcode", error); + return false; + } + } + + /** + * Remove all stored passcodes + */ + async clearPassCodes(): Promise { + if (!this.isConnected()) { + throw new Error("Lock is not connected"); + } + + if (!this.initialized) { + throw new Error("Lock is in pairing mode"); + } + + try { + if (await this.macro_adminLogin()) { + console.log("========= clear passCodes"); + const result = await this.clearCustomPasscodesCommand(); + console.log("========= clear passCodes", result); + return result; + } else { + return false; + } + } catch (error) { + console.error("Error while clearing passcodes", error); + return false; + } + } + + /** + * Get all valid passcodes + */ + async getPassCodes(): Promise { + if (!this.isConnected()) { + throw new Error("Lock is not connected"); + } + + if (!this.initialized) { + throw new Error("Lock is in pairing mode"); + } + + let data: KeyboardPassCode[] = []; + + try { + if (await this.macro_adminLogin()) { + let sequence = 0; + do { + console.log("========= get passCodes", sequence); + const response = await this.getCustomPasscodesCommand(sequence); + console.log("========= get passCodes", response); + sequence = response.sequence; + response.data.forEach((passageData) => { + data.push(passageData); + }); + } while (sequence != -1); + } + } catch (error) { + console.error("Error while getting passCodes", error); + } + + return data; + } + + /** + * Add an IC Card + * @param startDate Valid from YYYYMMDDHHmm + * @param endDate Valid to YYYYMMDDHHmm + * @returns serial number of the card that was added + */ + async addICCard(startDate: string, endDate: string): Promise { + if (!this.isConnected()) { + throw new Error("Lock is not connected"); + } + + if (!this.initialized) { + throw new Error("Lock is in pairing mode"); + } + + let data = ""; + + try { + if (await this.macro_adminLogin()) { + console.log("========= add IC Card"); + const cardNumber = await this.addICCommand(); + console.log("========= updating IC Card", cardNumber); + const response = await this.updateICCommand(cardNumber, startDate, endDate); + console.log("========= updating IC Card", response); + data = cardNumber; + } + } catch (error) { + console.error("Error while adding IC Card", error); + } + + return data; + } + + /** + * Update an IC Card + * @param cardNumber Serial number of the card + * @param startDate Valid from YYYYMMDDHHmm + * @param endDate Valid to YYYYMMDDHHmm + */ + async updateICCard(cardNumber: string, startDate: string, endDate: string): Promise { + if (!this.isConnected()) { + throw new Error("Lock is not connected"); + } + + if (!this.initialized) { + throw new Error("Lock is in pairing mode"); + } + + let data = false; + + try { + if (await this.macro_adminLogin()) { + console.log("========= updating IC Card", cardNumber); + const response = await this.updateICCommand(cardNumber, startDate, endDate); + console.log("========= updating IC Card", response); + data = response; + } + } catch (error) { + console.error("Error while updating IC Card", error); + } + + return data; + } + + /** + * Delete an IC Card + * @param cardNumber Serial number of the card + */ + async deleteICCard(cardNumber: string): Promise { + if (!this.isConnected()) { + throw new Error("Lock is not connected"); + } + + if (!this.initialized) { + throw new Error("Lock is in pairing mode"); + } + + let data = false; + + try { + if (await this.macro_adminLogin()) { + console.log("========= updating IC Card", cardNumber); + const response = await this.deleteICCommand(cardNumber); + console.log("========= updating IC Card", response); + data = response; + } + } catch (error) { + console.error("Error while adding IC Card", error); + } + + return data; + } + + /** + * Clear all IC Card data + */ + async clearICCards(): Promise { + if (!this.isConnected()) { + throw new Error("Lock is not connected"); + } + + if (!this.initialized) { + throw new Error("Lock is in pairing mode"); + } + + let data = false; + + try { + if (await this.macro_adminLogin()) { + console.log("========= clearing IC Cards"); + const response = await this.clearICCommand(); + console.log("========= clearing IC Cards", response); + data = response; + } + } catch (error) { + console.error("Error while clearing IC Cards", error); + } + + return data; + } + + /** + * Get all valid IC cards and their validity interval + */ + async getICCards(): Promise { + if (!this.isConnected()) { + throw new Error("Lock is not connected"); + } + + if (!this.initialized) { + throw new Error("Lock is in pairing mode"); + } + + let data: ICCard[] = []; + + try { + if (await this.macro_adminLogin()) { + let sequence = 0; + do { + console.log("========= get IC Cards", sequence); + const response = await this.getICCommand(sequence); + console.log("========= get IC Cards", response); + sequence = response.sequence; + response.data.forEach((card) => { + data.push(card); + }); + } while (sequence != -1); + } + } catch (error) { + console.error("Error while getting IC Cards", error); + } + + return data; + } + + /** + * Add a Fingerprint + * @param startDate Valid from YYYYMMDDHHmm + * @param endDate Valid to YYYYMMDDHHmm + * @returns serial number of the firngerprint that was added + */ + async addFingerprint(startDate: string, endDate: string): Promise { + if (!this.isConnected()) { + throw new Error("Lock is not connected"); + } + + if (!this.initialized) { + throw new Error("Lock is in pairing mode"); + } + + let data = ""; + + try { + if (await this.macro_adminLogin()) { + console.log("========= add Fingerprint"); + const fpNumber = await this.addFRCommand(); + console.log("========= updating Fingerprint", fpNumber); + const response = await this.updateFRCommand(fpNumber, startDate, endDate); + console.log("========= updating Fingerprint", response); + data = fpNumber; + } + } catch (error) { + console.error("Error while adding Fingerprint", error); + } + + return data; + } + + /** + * Update a fingerprint + * @param fpNumber Serial number of the fingerprint + * @param startDate Valid from YYYYMMDDHHmm + * @param endDate Valid to YYYYMMDDHHmm + */ + async updateFingerprint(fpNumber: string, startDate: string, endDate: string): Promise { + if (!this.isConnected()) { + throw new Error("Lock is not connected"); + } + + if (!this.initialized) { + throw new Error("Lock is in pairing mode"); + } + + let data = false; + + try { + if (await this.macro_adminLogin()) { + console.log("========= updating Fingerprint", fpNumber); + const response = await this.updateFRCommand(fpNumber, startDate, endDate); + console.log("========= updating Fingerprint", response); + data = response; + } + } catch (error) { + console.error("Error while updating Fingerprint", error); + } + + return data; + } + + /** + * Delete a fingerprint + * @param fpNumber Serial number of the fingerprint + */ + async deleteFingerprint(fpNumber: string): Promise { + if (!this.isConnected()) { + throw new Error("Lock is not connected"); + } + + if (!this.initialized) { + throw new Error("Lock is in pairing mode"); + } + + let data = false; + + try { + if (await this.macro_adminLogin()) { + console.log("========= updating Fingerprint", fpNumber); + const response = await this.deleteFRCommand(fpNumber); + console.log("========= updating Fingerprint", response); + data = response; + } + } catch (error) { + console.error("Error while adding Fingerprint", error); + } + + return data; + } + + /** + * Clear all fingerprint data + */ + async clearFingerprints(): Promise { + if (!this.isConnected()) { + throw new Error("Lock is not connected"); + } + + if (!this.initialized) { + throw new Error("Lock is in pairing mode"); + } + + let data = false; + + try { + if (await this.macro_adminLogin()) { + console.log("========= clearing Fingerprints"); + const response = await this.clearFRCommand(); + console.log("========= clearing Fingerprints", response); + data = response; + } + } catch (error) { + console.error("Error while clearing Fingerprints", error); + } + + return data; + } + + /** + * Get all valid IC cards and their validity interval + */ + async getFingerprints(): Promise { + if (!this.isConnected()) { + throw new Error("Lock is not connected"); + } + + if (!this.initialized) { + throw new Error("Lock is in pairing mode"); + } + + let data: Fingerprint[] = []; + + try { + if (await this.macro_adminLogin()) { + let sequence = 0; + do { + console.log("========= get Fingerprints", sequence); + const response = await this.getFRCommand(sequence); + console.log("========= get Fingerprints", response); + sequence = response.sequence; + response.data.forEach((fingerprint) => { + data.push(fingerprint); + }); + } while (sequence != -1); + } + } catch (error) { + console.error("Error while getting Fingerprints", error); + } + + return data; + } + + private onDataReceived(command: CommandEnvelope) { + // is this just a notification (like the lock was locked/unlocked etc.) + if (this.privateData.aesKey) { + command.setAesKey(this.privateData.aesKey); + console.log("Received:", command); + const data = command.getCommand().getRawData(); + if (data) { + console.log("Data", data.toString("hex")); + } + } else { + console.error("Unable to decrypt notification, no AES key"); + } + } + + private async onConnected(): Promise { + if (this.isPaired() && !this.skipDataRead) { + // read general data + console.log("Connected to known lock, reading general data"); + try { + // Search device features + console.log("========= feature list"); + this.featureList = await this.searchDeviceFeatureCommand(); + console.log("========= feature list", this.featureList); + + // Auto lock time + if (this.featureList.has(FeatureValue.AUTO_LOCK) && await this.macro_adminLogin()) { + console.log("========= autoLockTime"); + this.autoLockTime = await this.searchAutoLockTimeCommand(); + console.log("========= autoLockTime:", this.autoLockTime); + } + + // Locked/unlocked status + console.log("========= check lock status"); + this.lockedStatus = await this.searchBycicleStatusCommand(); + console.log("========= check lock status", this.lockedStatus); + } catch (error) { + console.error("Failed reading general data fromo lock", error); + } + } else { + if (this.device.isUnlock) { + this.lockedStatus = LockedStatus.UNLOCKED; + } else { + this.lockedStatus = LockedStatus.LOCKED; + } + } + this.connected = true; + this.emit("connected", this); + } + + private async onDisconnected(): Promise { + this.connected = false; + this.emit("disconnected", this); + } + + getLockData(): TTLockData | void { + if (this.isPaired()) { + const privateData: TTLockPrivateData = { + aesKey: this.privateData.aesKey?.toString("hex"), + admin: this.privateData.admin, + adminPasscode: this.privateData.adminPasscode, + pwdInfo: this.privateData.pwdInfo + } + const data: TTLockData = { + address: this.device.address, + battery: this.batteryCapacity, + rssi: this.rssi, + autoLockTime: this.autoLockTime ? this.autoLockTime : -1, + lockedStatus: this.lockedStatus, + privateData: privateData, + }; + return data; + } + } + + /** Just for debugging */ + toJSON(asObject: boolean = false): string | Object { + let json: Object = this.device.toJSON(true); + + if (this.featureList) Reflect.set(json, 'featureList', this.featureList); + if (this.switchState) Reflect.set(json, 'switchState', this.switchState); + if (this.lockSound) Reflect.set(json, 'lockSound', this.lockSound); + if (this.displayPasscode) Reflect.set(json, 'displayPasscode', this.displayPasscode); + if (this.autoLockTime) Reflect.set(json, 'autoLockTime', this.autoLockTime); + if (this.lightingTime) Reflect.set(json, 'lightingTime', this.lightingTime); + if (this.remoteUnlock) Reflect.set(json, 'remoteUnlock', this.remoteUnlock); + if (this.deviceInfo) Reflect.set(json, 'deviceInfo', this.deviceInfo); + const privateData: Object = {}; + if (this.privateData.aesKey) Reflect.set(privateData, 'aesKey', this.privateData.aesKey.toString("hex")); + if (this.privateData.admin) Reflect.set(privateData, 'admin', this.privateData.admin); + if (this.privateData.adminPasscode) Reflect.set(privateData, 'adminPasscode', this.privateData.adminPasscode); + if (this.privateData.pwdInfo) Reflect.set(privateData, 'pwdInfo', this.privateData.pwdInfo); + Reflect.set(json, 'privateData', privateData); + + if (asObject) { + return json; + } else { + return JSON.stringify(json); + } + } +} \ No newline at end of file diff --git a/src/device/TTLockApi.ts b/src/device/TTLockApi.ts new file mode 100644 index 0000000..aaf61d3 --- /dev/null +++ b/src/device/TTLockApi.ts @@ -0,0 +1,1262 @@ +'use strict'; + +import { EventEmitter } from "events"; +import { CommandEnvelope, KeyboardPwdType, TTLockData } from ".."; +import { AudioManage } from "../constant/AudioManage"; +import { CommandResponse } from "../constant/CommandResponse"; +import { CommandType } from "../constant/CommandType"; +import { ConfigRemoteUnlock } from "../constant/ConfigRemoteUnlock"; +import { FeatureValue } from "../constant/FeatureValue"; +import { defaultAESKey } from "../util/AESUtil"; +import { DeviceInfoType } from "./DeviceInfoType"; +import { PrivateDataType } from "./PrivateDataType"; +import { TTBluetoothDevice } from "./TTBluetoothDevice"; +import { + AddAdminCommand, AESKeyCommand, AudioManageCommand, + InitPasswordsCommand, ScreenPasscodeManageCommand, SetAdminKeyboardPwdCommand, + ControlRemoteUnlockCommand, DeviceFeaturesCommand, OperateFinishedCommand, + ReadDeviceInfoCommand, AutoLockManageCommand, GetAdminCodeCommand, + CheckAdminCommand, CheckRandomCommand, CheckUserTimeCommand, + UnlockDataInterface, UnlockCommand, LockCommand, + PassageModeCommand, PassageModeData, SearchBicycleStatusCommand, + ManageKeyboardPasswordCommand, GetKeyboardPasswordsCommand, KeyboardPassCode, + ICCard, ManageICCommand, ManageFRCommand, Fingerprint +} from "../api/Commands"; +import { PassageModeOperate } from "../constant/PassageModeOperate"; +import { AdminType } from "./AdminType"; +import { CodeSecret } from "../api/Commands/InitPasswordsCommand"; +import { DeviceInfoEnum } from "../constant/DeviceInfoEnum"; +import { ICOperate } from "../constant/ICOperate"; +import { LockedStatus } from "../constant/LockedStatus"; + +export interface PassageModeResponse { + sequence: number; + data: PassageModeData[]; +} + +export interface PassCodesResponse { + sequence: number; + data: KeyboardPassCode[]; +} + +export interface ICCardResponse { + sequence: number; + data: ICCard[]; +} + +export interface FingerprintResponse { + sequence: number; + data: Fingerprint[]; +} + +export abstract class TTLockApi extends EventEmitter { + protected initialized: boolean = false; + protected device: TTBluetoothDevice; + + // discoverable stuff + protected featureList?: Set; + protected switchState?: any; + protected lockSound?: AudioManage.TURN_ON | AudioManage.TURN_OFF + protected displayPasscode?: 0 | 1; + protected autoLockTime: number; + protected batteryCapacity: number; + protected rssi: number; + protected lightingTime?: number; + protected remoteUnlock?: ConfigRemoteUnlock.OP_OPEN | ConfigRemoteUnlock.OP_CLOSE; + protected lockedStatus: LockedStatus; + protected deviceInfo?: DeviceInfoType; + + // sensitive data + protected privateData: PrivateDataType; + + constructor(device: TTBluetoothDevice, data?: TTLockData) { + super(); + this.device = device; + this.privateData = {}; + this.lockedStatus = LockedStatus.UNKNOWN; + this.autoLockTime = -1; + this.batteryCapacity = this.device.batteryCapacity; + this.rssi = this.device.rssi; + if (typeof data != "undefined") { + const privateData = data.privateData; + if (privateData.aesKey) { + this.privateData.aesKey = Buffer.from(privateData.aesKey, "hex"); + } + this.privateData.admin = privateData.admin; + this.privateData.adminPasscode = privateData.adminPasscode; + this.privateData.pwdInfo = privateData.pwdInfo; + this.initialized = true; + } else { + this.initialized = !device.isSettingMode; + } + } + + /** + * Send init command + */ + protected async initCommand(): Promise { + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, defaultAESKey); + requestEnvelope.setCommandType(CommandType.COMM_INITIALIZATION); + const responseEnvelope = await this.device.sendCommand(requestEnvelope); + if (responseEnvelope) { + + } else { + throw new Error("No response to init"); + } + } + + /** + * Send get AESKey command + */ + protected async getAESKeyCommand(): Promise { + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, defaultAESKey); + requestEnvelope.setCommandType(CommandType.COMM_GET_AES_KEY); + const responseEnvelope = await this.device.sendCommand(requestEnvelope); + if (responseEnvelope) { + responseEnvelope.setAesKey(defaultAESKey); + let cmd = responseEnvelope.getCommand(); + if (cmd.getResponse() != CommandResponse.SUCCESS) { + throw new Error("Failed getting AES key from lock"); + } + if (cmd instanceof AESKeyCommand) { + const command = cmd as AESKeyCommand; + const aesKey = command.getAESKey(); + if (aesKey) { + return aesKey; + } else { + throw new Error("Unable to getAESKey"); + } + } else { + throw new Error("Invalid response to getAESKey"); + } + } else { + throw new Error("No response to getAESKey"); + } + } + + /** + * Send AddAdmin command + */ + protected async addAdminCommand(aesKey?: Buffer): Promise { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_ADD_ADMIN); + const addAdminCommand = requestEnvelope.getCommand() as AddAdminCommand; + const admin: AdminType = { + adminPs: addAdminCommand.setAdminPs(), + unlockKey: addAdminCommand.setUnlockKey(), + } + console.log("Setting adminPs", admin.adminPs, "and unlockKey", admin.unlockKey); + const responseEnvelope = await this.device.sendCommand(requestEnvelope); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + const cmd = responseEnvelope.getCommand(); + if (cmd.getResponse() != CommandResponse.SUCCESS) { + throw new Error("Failed AddAdmin"); + } + return admin; + } else { + throw new Error("No response to AddAdmin"); + } + } + + /** + * Send CalibrationTime command + */ + protected async calibrateTimeCommand(aesKey?: Buffer): Promise { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_TIME_CALIBRATE); + const responseEnvelope = await this.device.sendCommand(requestEnvelope, true, true); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + const cmd = responseEnvelope.getCommand(); + if (cmd.getResponse() != CommandResponse.SUCCESS) { + throw new Error("Failed setting lock time"); + } + } else { + throw new Error("No response to time calibration"); + } + } + + /** + * Send SearchDeviceFeature command + */ + protected async searchDeviceFeatureCommand(aesKey?: Buffer): Promise> { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_SEARCHE_DEVICE_FEATURE); + const responseEnvelope = await this.device.sendCommand(requestEnvelope); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + const cmd = responseEnvelope.getCommand() as DeviceFeaturesCommand; + if (cmd.getResponse() != CommandResponse.SUCCESS) { + throw new Error("Failed to search device features"); + } + return cmd.getFeaturesList(); + } else { + throw new Error("No response to search device features"); + } + } + + protected async getSwitchStateCommand(newValue?: any, aesKey?: Buffer): Promise { + throw new Error("Method not implemented."); + } + + /** + * Send AudioManage command to get or set the audio feedback + */ + protected async audioManageCommand(newValue?: AudioManage.TURN_ON | AudioManage.TURN_OFF, aesKey?: Buffer): Promise { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_AUDIO_MANAGE); + if (typeof newValue != "undefined") { + const cmd = requestEnvelope.getCommand() as AudioManageCommand; + cmd.setNewValue(newValue); + } + const responseEnvelope = await this.device.sendCommand(requestEnvelope); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + const cmd = responseEnvelope.getCommand() as AudioManageCommand; + if (cmd.getResponse() != CommandResponse.SUCCESS) { + throw new Error("Failed to set audio mode"); + } + if (typeof newValue != "undefined") { + return newValue; + } else { + const value = cmd.getValue(); + if (value) { + return value; + } else { + throw new Error("Unable to get audioManage value"); + } + } + } else { + throw new Error("No response to get audioManage"); + } + } + + /** + * Send ScreenPasscodeManage command to get or set password display + */ + protected async screenPasscodeManageCommand(newValue?: 0 | 1, aesKey?: Buffer): Promise<0 | 1> { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_SHOW_PASSWORD); + if (typeof newValue != "undefined") { + const cmd = requestEnvelope.getCommand() as ScreenPasscodeManageCommand; + cmd.setNewValue(newValue); + } + const responseEnvelope = await this.device.sendCommand(requestEnvelope); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + const cmd = responseEnvelope.getCommand() as ScreenPasscodeManageCommand; + if (cmd.getResponse() != CommandResponse.SUCCESS) { + throw new Error("Failed to set screenPasscode mode"); + } + if (typeof newValue != "undefined") { + return newValue; + } else { + const value = cmd.getValue(); + if (value) { + return value; + } else { + throw new Error("Unable to get screenPasscode value"); + } + } + } else { + throw new Error("No response to get screenPasscode"); + } + } + + protected async searchAutoLockTimeCommand(newValue?: any, aesKey?: Buffer): Promise { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_AUTO_LOCK_MANAGE); + if (typeof newValue != "undefined") { + const cmd = requestEnvelope.getCommand() as AutoLockManageCommand; + cmd.setTime(newValue); + } + const responseEnvelope = await this.device.sendCommand(requestEnvelope); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + const cmd = responseEnvelope.getCommand() as AutoLockManageCommand; + if (cmd.getResponse() != CommandResponse.SUCCESS) { + throw new Error("Failed to set/get autoLockTime"); + } + return cmd.getTime(); + } else { + throw new Error("No response to autoLockTime"); + } + } + + protected async controlLampCommand(newValue?: any, aesKey?: Buffer): Promise { + throw new Error("Method not implemented."); + } + + protected async getAdminCodeCommand(aesKey?: Buffer): Promise { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_GET_ADMIN_CODE); + const responseEnvelope = await this.device.sendCommand(requestEnvelope); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + const cmd = responseEnvelope.getCommand() as GetAdminCodeCommand; + if (cmd.getResponse() != CommandResponse.SUCCESS) { + throw new Error("Failed to set adminPasscode"); + } + const adminPasscode = cmd.getAdminPasscode(); + if (adminPasscode) { + return adminPasscode; + } else { + return ""; + } + } else { + throw new Error("No response to get adminPasscode"); + } + } + + /** + * Send SetAdminKeyboardPwd + */ + protected async setAdminKeyboardPwdCommand(adminPasscode?: string, aesKey?: Buffer): Promise { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + if (typeof adminPasscode == "undefined") { + adminPasscode = ""; + for (let i = 0; i < 7; i++) { + adminPasscode += (Math.floor(Math.random() * 10)).toString(); + } + console.log("Generated adminPasscode:", adminPasscode); + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_SET_ADMIN_KEYBOARD_PWD); + let cmd = requestEnvelope.getCommand() as SetAdminKeyboardPwdCommand; + cmd.setAdminPasscode(adminPasscode); + const responseEnvelope = await this.device.sendCommand(requestEnvelope); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + cmd = responseEnvelope.getCommand() as SetAdminKeyboardPwdCommand; + if (cmd.getResponse() != CommandResponse.SUCCESS) { + throw new Error("Failed to set adminPasscode"); + } + return adminPasscode; + } else { + throw new Error("No response to set adminPasscode"); + } + } + + /** + * Send InitPasswords command + */ + protected async initPasswordsCommand(aesKey?: Buffer): Promise { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_INIT_PASSWORDS); + let cmd = requestEnvelope.getCommand() as InitPasswordsCommand; + const responseEnvelope = await this.device.sendCommand(requestEnvelope); + if (responseEnvelope) { + const pwdInfo = cmd.getPwdInfo(); + if (pwdInfo) { + responseEnvelope.setAesKey(aesKey); + cmd = responseEnvelope.getCommand() as InitPasswordsCommand; + if (cmd.getResponse() != CommandResponse.SUCCESS) { + console.error(pwdInfo); + throw new Error("Failed to init passwords"); + } + return pwdInfo; + } else { + throw new Error("Failed generating pwdInfo"); + } + } else { + throw new Error("No response to initPasswords"); + } + } + + /** + * Send ControlRemoteUnlock command to activate or disactivate remote unlock (via gateway?) + */ + protected async controlRemoteUnlockCommand(newValue?: ConfigRemoteUnlock.OP_CLOSE | ConfigRemoteUnlock.OP_OPEN, aesKey?: Buffer): Promise { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_CONTROL_REMOTE_UNLOCK); + if (typeof newValue != "undefined") { + const cmd = requestEnvelope.getCommand() as ControlRemoteUnlockCommand; + cmd.setNewValue(newValue); + } + const responseEnvelope = await this.device.sendCommand(requestEnvelope); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + const cmd = responseEnvelope.getCommand() as ControlRemoteUnlockCommand; + if (cmd.getResponse() != CommandResponse.SUCCESS) { + throw new Error("Failed to set remote unlock"); + } + if (typeof newValue != "undefined") { + return newValue; + } else { + const value = cmd.getValue(); + if (value) { + return value; + } else { + throw new Error("Unable to get remote unlock value"); + } + } + } else { + throw new Error("No response to get remote unlock"); + } + } + + /** + * Send OperateFinished command + */ + protected async operateFinishedCommand(aesKey?: Buffer): Promise { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_GET_ALARM_ERRCORD_OR_OPERATION_FINISHED); + const responseEnvelope = await this.device.sendCommand(requestEnvelope); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + const cmd = responseEnvelope.getCommand() as OperateFinishedCommand; + if (cmd.getResponse() != CommandResponse.SUCCESS) { + throw new Error("Failed to set operateFinished"); + } + } else { + throw new Error("No response to operateFinished"); + } + } + + protected async readDeviceInfoCommand(infoType: DeviceInfoEnum, aesKey?: Buffer): Promise { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_READ_DEVICE_INFO); + let cmd = requestEnvelope.getCommand() as ReadDeviceInfoCommand; + cmd.setInfoType(infoType); + const responseEnvelope = await this.device.sendCommand(requestEnvelope); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + cmd = responseEnvelope.getCommand() as ReadDeviceInfoCommand; + if (cmd.getResponse() != CommandResponse.SUCCESS) { + console.error("Failed deviceInfo response"); + // throw new Error("Failed deviceInfo response"); + } + const infoData = cmd.getInfoData(); + if (infoData) { + return infoData; + } else { + return Buffer.from([]); + } + } else { + throw new Error("No response to deviceInfo"); + } + } + + protected async checkAdminCommand(aesKey?: Buffer): Promise { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + if (typeof this.privateData.admin == "undefined" || typeof this.privateData.admin.adminPs == "undefined") { + throw new Error("Admin data is not set"); + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_CHECK_ADMIN); + let cmd = requestEnvelope.getCommand() as CheckAdminCommand; + cmd.setParams(this.privateData.admin.adminPs); + const responseEnvelope = await this.device.sendCommand(requestEnvelope); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + cmd = responseEnvelope.getCommand() as CheckAdminCommand; + if (cmd.getResponse() != CommandResponse.SUCCESS) { + throw new Error("Failed checkAdmin response"); + } + return cmd.getPsFromLock(); + } else { + throw new Error("No response to checkAdmin"); + } + } + + protected async checkRandomCommand(psFromLock: number, aesKey?: Buffer): Promise { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + if (typeof this.privateData.admin == "undefined" || typeof this.privateData.admin.unlockKey == "undefined") { + throw new Error("Admin data is not set"); + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_CHECK_RANDOM); + let cmd = requestEnvelope.getCommand() as CheckRandomCommand; + cmd.setSum(psFromLock, this.privateData.admin.unlockKey); + const responseEnvelope = await this.device.sendCommand(requestEnvelope); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + cmd = responseEnvelope.getCommand() as CheckRandomCommand; + if (cmd.getResponse() != CommandResponse.SUCCESS) { + throw new Error("Failed checkRandom response"); + } + } else { + throw new Error("No response to checkRandom"); + } + } + + protected async resetLockCommand(aesKey?: Buffer): Promise { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_RESET_LOCK); + const responseEnvelope = await this.device.sendCommand(requestEnvelope); + if (responseEnvelope) { + // reset returns an empty response + } else { + throw new Error("No response to resetLock"); + } + } + + protected async checkUserTime(startDate: string = '0001311400', endDate: string = "9911301400", aesKey?: Buffer): Promise { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_CHECK_USER_TIME); + let cmd = requestEnvelope.getCommand() as CheckUserTimeCommand; + cmd.setPayload(0, startDate, endDate, 0); + const responseEnvelope = await this.device.sendCommand(requestEnvelope); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + cmd = responseEnvelope.getCommand() as CheckUserTimeCommand; + if (cmd.getResponse() != CommandResponse.SUCCESS) { + throw new Error("Failed checkUserTime response"); + } + return cmd.getPsFromLock(); + } else { + throw new Error("No response to checkUserTime"); + } + } + + protected async unlockCommand(psFromLock: number, aesKey?: Buffer): Promise { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + if (typeof this.privateData.admin == "undefined" || typeof this.privateData.admin.unlockKey == "undefined") { + throw new Error("Admin data is not set"); + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_UNLOCK); + let cmd = requestEnvelope.getCommand() as UnlockCommand; + cmd.setSum(psFromLock, this.privateData.admin.unlockKey); + const responseEnvelope = await this.device.sendCommand(requestEnvelope); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + cmd = responseEnvelope.getCommand() as UnlockCommand; + if (cmd.getResponse() != CommandResponse.SUCCESS) { + throw new Error("Failed unlock response"); + } + this.batteryCapacity = cmd.getBatteryCapacity(); + return cmd.getUnlockData(); + } else { + throw new Error("No response to unlock"); + } + } + + protected async lockCommand(psFromLock: number, aesKey?: Buffer): Promise { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + if (typeof this.privateData.admin == "undefined" || typeof this.privateData.admin.unlockKey == "undefined") { + throw new Error("Admin data is not set"); + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_FUNCTION_LOCK); + let cmd = requestEnvelope.getCommand() as LockCommand; + cmd.setSum(psFromLock, this.privateData.admin.unlockKey); + const responseEnvelope = await this.device.sendCommand(requestEnvelope); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + cmd = responseEnvelope.getCommand() as LockCommand; + if (cmd.getResponse() != CommandResponse.SUCCESS) { + throw new Error("Failed unlock response"); + } + this.batteryCapacity = cmd.getBatteryCapacity(); + return cmd.getUnlockData(); + } else { + throw new Error("No response to unlock"); + } + } + + protected async getPassageModeCommand(sequence: number = 0, aesKey?: Buffer): Promise { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_CONFIGURE_PASSAGE_MODE); + let cmd = requestEnvelope.getCommand() as PassageModeCommand; + cmd.setSequence(sequence); + const responseEnvelope = await this.device.sendCommand(requestEnvelope); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + cmd = responseEnvelope.getCommand() as PassageModeCommand; + if (cmd.getResponse() != CommandResponse.SUCCESS) { + throw new Error("Failed get passage mode response"); + } + return { + sequence: cmd.getSequence(), + data: cmd.getData() + } + } else { + throw new Error("No response to get passage mode"); + } + } + + protected async setPassageModeCommand(data: PassageModeData, type: PassageModeOperate.ADD | PassageModeOperate.DELETE = PassageModeOperate.ADD, aesKey?: Buffer): Promise { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_CONFIGURE_PASSAGE_MODE); + let cmd = requestEnvelope.getCommand() as PassageModeCommand; + cmd.setData(data, type); + const responseEnvelope = await this.device.sendCommand(requestEnvelope); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + cmd = responseEnvelope.getCommand() as PassageModeCommand; + if (cmd.getResponse() != CommandResponse.SUCCESS) { + throw new Error("Failed set passage mode response"); + } + return true; + } else { + throw new Error("No response to set passage mode"); + } + } + + protected async clearPassageModeCommand(aesKey?: Buffer): Promise { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_CONFIGURE_PASSAGE_MODE); + let cmd = requestEnvelope.getCommand() as PassageModeCommand; + cmd.setClear(); + const responseEnvelope = await this.device.sendCommand(requestEnvelope); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + cmd = responseEnvelope.getCommand() as PassageModeCommand; + if (cmd.getResponse() != CommandResponse.SUCCESS) { + throw new Error("Failed clear passage mode response"); + } + return true; + } else { + throw new Error("No response to clear passage mode"); + } + } + + protected async searchBycicleStatusCommand(aesKey?: Buffer): Promise { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_SEARCH_BICYCLE_STATUS); + const responseEnvelope = await this.device.sendCommand(requestEnvelope); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + const cmd = responseEnvelope.getCommand() as SearchBicycleStatusCommand; + if (cmd.getResponse() != CommandResponse.SUCCESS) { + throw new Error("Failed search status response"); + } + return cmd.getLockStatus(); + } else { + throw new Error("No response to search status"); + } + } + + protected async createCustomPasscodeCommand(type: KeyboardPwdType, passCode: string, startDate?: string, endDate?: string, aesKey?: Buffer): Promise { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_MANAGE_KEYBOARD_PASSWORD); + let cmd = requestEnvelope.getCommand() as ManageKeyboardPasswordCommand; + cmd.addPasscode(type, passCode, startDate, endDate); + const responseEnvelope = await this.device.sendCommand(requestEnvelope); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + const cmd = responseEnvelope.getCommand() as ManageKeyboardPasswordCommand; + if (cmd.getResponse() != CommandResponse.SUCCESS) { + throw new Error("Failed create passcode response"); + } + return true; + } else { + throw new Error("No response to create passcode"); + } + } + + protected async updateCustomPasscodeCommand(type: KeyboardPwdType, oldPassCode: string, newPassCode: string, startDate?: string, endDate?: string, aesKey?: Buffer): Promise { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_MANAGE_KEYBOARD_PASSWORD); + let cmd = requestEnvelope.getCommand() as ManageKeyboardPasswordCommand; + cmd.updatePasscode(type, oldPassCode, newPassCode, startDate, endDate); + const responseEnvelope = await this.device.sendCommand(requestEnvelope); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + const cmd = responseEnvelope.getCommand() as ManageKeyboardPasswordCommand; + if (cmd.getResponse() != CommandResponse.SUCCESS) { + throw new Error("Failed update passcode response"); + } + return true; + } else { + throw new Error("No response to update passcode"); + } + } + + protected async deleteCustomPasscodeCommand(type: KeyboardPwdType, passCode: string, aesKey?: Buffer): Promise { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_MANAGE_KEYBOARD_PASSWORD); + let cmd = requestEnvelope.getCommand() as ManageKeyboardPasswordCommand; + cmd.deletePasscode(type, passCode); + const responseEnvelope = await this.device.sendCommand(requestEnvelope); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + const cmd = responseEnvelope.getCommand() as ManageKeyboardPasswordCommand; + if (cmd.getResponse() != CommandResponse.SUCCESS) { + throw new Error("Failed delete passcode response"); + } + return true; + } else { + throw new Error("No response to delete passcode"); + } + } + + protected async clearCustomPasscodesCommand(aesKey?: Buffer): Promise { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_MANAGE_KEYBOARD_PASSWORD); + let cmd = requestEnvelope.getCommand() as ManageKeyboardPasswordCommand; + cmd.clearAllPasscodes(); + const responseEnvelope = await this.device.sendCommand(requestEnvelope); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + const cmd = responseEnvelope.getCommand() as ManageKeyboardPasswordCommand; + if (cmd.getResponse() != CommandResponse.SUCCESS) { + throw new Error("Failed clear passcodes response"); + } + return true; + } else { + throw new Error("No response to clear passcodes"); + } + } + + protected async getCustomPasscodesCommand(sequence: number = 0, aesKey?: Buffer): Promise { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_PWD_LIST); + let cmd = requestEnvelope.getCommand() as GetKeyboardPasswordsCommand; + cmd.setSequence(sequence); + const responseEnvelope = await this.device.sendCommand(requestEnvelope, true, true); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + cmd = responseEnvelope.getCommand() as GetKeyboardPasswordsCommand; + if (cmd.getResponse() != CommandResponse.SUCCESS) { + throw new Error("Failed get passCodes response"); + } + return { + sequence: cmd.getSequence(), + data: cmd.getPasscodes() + } + } else { + throw new Error("No response to get passCodes"); + } + } + + protected async getICCommand(sequence: number = 0, aesKey?: Buffer): Promise { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_IC_MANAGE); + let cmd = requestEnvelope.getCommand() as ManageICCommand; + cmd.setSequence(sequence); + const responseEnvelope = await this.device.sendCommand(requestEnvelope, true, true); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + cmd = responseEnvelope.getCommand() as ManageICCommand; + if (cmd.getResponse() != CommandResponse.SUCCESS) { + throw new Error("Failed get IC response"); + } + this.batteryCapacity = cmd.getBatteryCapacity(); + return { + sequence: cmd.getSequence(), + data: cmd.getCards() + } + } else { + throw new Error("No response to get IC"); + } + } + + protected async addICCommand(aesKey?: Buffer): Promise { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_IC_MANAGE); + let cmd = requestEnvelope.getCommand() as ManageICCommand; + cmd.setAdd(); + let responseEnvelope = await this.device.sendCommand(requestEnvelope); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + cmd = responseEnvelope.getCommand() as ManageICCommand; + if (cmd.getResponse() != CommandResponse.SUCCESS || cmd.getType() != ICOperate.STATUS_ENTER_ADD_MODE) { + throw new Error("Failed add IC mode response"); + } + this.emit("scanICStart", this); + responseEnvelope = await this.device.waitForResponse(); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + cmd = responseEnvelope.getCommand() as ManageICCommand; + if (cmd.getResponse() != CommandResponse.SUCCESS || cmd.getType() != ICOperate.STATUS_ADD_SUCCESS) { + throw new Error("Failed add IC response"); + } + this.batteryCapacity = cmd.getBatteryCapacity(); + return cmd.getCardNumber(); + } else { + throw new Error("No response to add IC"); + } + } else { + throw new Error("No response to add IC mode"); + } + } + + protected async updateICCommand(cardNumber: string, startDate: string, endDate: string, aesKey?: Buffer): Promise { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_IC_MANAGE); + let cmd = requestEnvelope.getCommand() as ManageICCommand; + cmd.setModify(cardNumber, startDate, endDate); + const responseEnvelope = await this.device.sendCommand(requestEnvelope); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + cmd = responseEnvelope.getCommand() as ManageICCommand; + if (cmd.getResponse() != CommandResponse.SUCCESS) { + throw new Error("Failed update IC"); + } + this.batteryCapacity = cmd.getBatteryCapacity(); + return true; + } else { + throw new Error("No response to update IC"); + } + } + + protected async deleteICCommand(cardNumber: string, aesKey?: Buffer): Promise { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_IC_MANAGE); + let cmd = requestEnvelope.getCommand() as ManageICCommand; + cmd.setDelete(cardNumber); + const responseEnvelope = await this.device.sendCommand(requestEnvelope); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + cmd = responseEnvelope.getCommand() as ManageICCommand; + if (cmd.getResponse() != CommandResponse.SUCCESS) { + throw new Error("Failed delete IC"); + } + this.batteryCapacity = cmd.getBatteryCapacity(); + return true; + } else { + throw new Error("No response to delete IC"); + } + } + + protected async clearICCommand(aesKey?: Buffer): Promise { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_IC_MANAGE); + let cmd = requestEnvelope.getCommand() as ManageICCommand; + cmd.setClear(); + const responseEnvelope = await this.device.sendCommand(requestEnvelope); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + cmd = responseEnvelope.getCommand() as ManageICCommand; + if (cmd.getResponse() != CommandResponse.SUCCESS) { + throw new Error("Failed clear IC"); + } + this.batteryCapacity = cmd.getBatteryCapacity(); + return true; + } else { + throw new Error("No response to clear IC"); + } + } + + protected async getFRCommand(sequence: number = 0, aesKey?: Buffer): Promise { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_FR_MANAGE); + let cmd = requestEnvelope.getCommand() as ManageFRCommand; + cmd.setSequence(sequence); + const responseEnvelope = await this.device.sendCommand(requestEnvelope, true, true); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + cmd = responseEnvelope.getCommand() as ManageFRCommand; + if (cmd.getResponse() != CommandResponse.SUCCESS) { + throw new Error("Failed get FR response"); + } + this.batteryCapacity = cmd.getBatteryCapacity(); + return { + sequence: cmd.getSequence(), + data: cmd.getFingerprints() + } + } else { + throw new Error("No response to get FR"); + } + } + + protected async addFRCommand(aesKey?: Buffer): Promise { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_FR_MANAGE); + let cmd = requestEnvelope.getCommand() as ManageFRCommand; + cmd.setAdd(); + let responseEnvelope = await this.device.sendCommand(requestEnvelope); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + cmd = responseEnvelope.getCommand() as ManageFRCommand; + if (cmd.getResponse() != CommandResponse.SUCCESS || cmd.getType() != ICOperate.STATUS_ENTER_ADD_MODE) { + throw new Error("Failed add FR mode response"); + } + this.emit("scanFRStart", this); + + // Fingerprint scanning progress + do { + responseEnvelope = await this.device.waitForResponse(); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + cmd = responseEnvelope.getCommand() as ManageFRCommand; + if (cmd.getType() == ICOperate.STATUS_FR_PROGRESS) { + this.emit("scanFRProgress", this); + } + } else { + throw new Error("No response to add FR progress"); + } + } while (cmd.getResponse() == CommandResponse.SUCCESS && cmd.getType() == ICOperate.STATUS_FR_PROGRESS); + + if (cmd.getResponse() != CommandResponse.SUCCESS) { + throw new Error("Failed during FR progress"); + } + if (cmd.getType() != ICOperate.STATUS_ADD_SUCCESS) { + throw new Error("Failed to add FR"); + } + this.batteryCapacity = cmd.getBatteryCapacity(); + return cmd.getFpNumber(); + } else { + throw new Error("No response to add FR mode"); + } + } + + protected async updateFRCommand(fpNumber: string, startDate: string, endDate: string, aesKey?: Buffer): Promise { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_FR_MANAGE); + let cmd = requestEnvelope.getCommand() as ManageFRCommand; + cmd.setModify(fpNumber, startDate, endDate); + const responseEnvelope = await this.device.sendCommand(requestEnvelope); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + cmd = responseEnvelope.getCommand() as ManageFRCommand; + if (cmd.getResponse() != CommandResponse.SUCCESS) { + throw new Error("Failed update FR"); + } + this.batteryCapacity = cmd.getBatteryCapacity(); + return true; + } else { + throw new Error("No response to update FR"); + } + } + + protected async deleteFRCommand(fpNumber: string, aesKey?: Buffer): Promise { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_FR_MANAGE); + let cmd = requestEnvelope.getCommand() as ManageFRCommand; + cmd.setDelete(fpNumber); + const responseEnvelope = await this.device.sendCommand(requestEnvelope); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + cmd = responseEnvelope.getCommand() as ManageFRCommand; + if (cmd.getResponse() != CommandResponse.SUCCESS) { + throw new Error("Failed delete FR"); + } + this.batteryCapacity = cmd.getBatteryCapacity(); + return true; + } else { + throw new Error("No response to delete FR"); + } + } + + protected async clearFRCommand(aesKey?: Buffer): Promise { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + const requestEnvelope = CommandEnvelope.createFromLockType(this.device.lockType, aesKey); + requestEnvelope.setCommandType(CommandType.COMM_FR_MANAGE); + let cmd = requestEnvelope.getCommand() as ManageFRCommand; + cmd.setClear(); + const responseEnvelope = await this.device.sendCommand(requestEnvelope); + if (responseEnvelope) { + responseEnvelope.setAesKey(aesKey); + cmd = responseEnvelope.getCommand() as ManageFRCommand; + if (cmd.getResponse() != CommandResponse.SUCCESS) { + throw new Error("Failed clear FR"); + } + this.batteryCapacity = cmd.getBatteryCapacity(); + return true; + } else { + throw new Error("No response to clear FR"); + } + } + + protected async macro_readAllDeviceInfo(aesKey?: Buffer): Promise { + if (typeof aesKey == "undefined") { + if (this.privateData.aesKey) { + aesKey = this.privateData.aesKey; + } else { + throw new Error("No AES key for lock"); + } + } + + const deviceInfo: DeviceInfoType = { + featureValue: "", + modelNum: "", + hardwareRevision: "", + firmwareRevision: "", + nbNodeId: "", + nbOperator: "", + nbCardNumber: "", + nbRssi: -1, + factoryDate: "", + lockClock: "", + } + + deviceInfo.modelNum = (await this.readDeviceInfoCommand(DeviceInfoEnum.MODEL_NUMBER, aesKey)).toString(); + deviceInfo.hardwareRevision = (await this.readDeviceInfoCommand(DeviceInfoEnum.HARDWARE_REVISION, aesKey)).toString(); + deviceInfo.firmwareRevision = (await this.readDeviceInfoCommand(DeviceInfoEnum.FIRMWARE_REVISION, aesKey)).toString(); + deviceInfo.factoryDate = (await this.readDeviceInfoCommand(DeviceInfoEnum.MANUFACTURE_DATE, aesKey)).toString(); + if (this.featureList && this.featureList.has(FeatureValue.NB_LOCK)) { + deviceInfo.nbOperator = (await this.readDeviceInfoCommand(DeviceInfoEnum.NB_OPERATOR, aesKey)).toString(); + deviceInfo.nbNodeId = (await this.readDeviceInfoCommand(DeviceInfoEnum.NB_IMEI, aesKey)).toString(); + deviceInfo.nbCardNumber = (await this.readDeviceInfoCommand(DeviceInfoEnum.NB_CARD_INFO, aesKey)).toString(); + deviceInfo.nbRssi = (await this.readDeviceInfoCommand(DeviceInfoEnum.NB_RSSI, aesKey)).readInt8(0); + } + + return deviceInfo; + } + + protected async macro_adminLogin(): Promise { + try { + console.log("========= check admin"); + const psFromLock = await this.checkAdminCommand(); + console.log("========= check admin:", psFromLock); + if (psFromLock > 0) { + console.log("========= check random"); + await this.checkRandomCommand(psFromLock); + console.log("========= check random"); + return true; + } else { + console.error("Invalid psFromLock received", psFromLock); + } + } catch (error) { + console.error("macro_adminLogin:", error); + } + return false; + } +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..cdf3224 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,15 @@ +'use strict'; + +export { TTLockClient } from "./TTLockClient"; +export { TTLock } from "./device/TTLock"; +export { LockedStatus } from "./constant/LockedStatus"; +export { TTLockData } from "./store/TTLockData"; +export { ValidityInfo } from "./api/ValidityInfo"; +export { PassageModeData, KeyboardPassCode, ICCard } from "./api/Commands"; +export { PassageModeType } from "./constant/PassageModeType"; +export { KeyboardPwdType } from "./constant/KeyboardPwdType"; + +// extra stuff used in testing +export * from "./api/Commands"; +export { CommandEnvelope } from "./api/CommandEnvelope"; +export * from "./util/timingUtil"; \ No newline at end of file diff --git a/src/scanner/BluetoothLeService.ts b/src/scanner/BluetoothLeService.ts new file mode 100644 index 0000000..2305ced --- /dev/null +++ b/src/scanner/BluetoothLeService.ts @@ -0,0 +1,64 @@ +'use strict'; + +import { EventEmitter } from "events"; +import { ScannerInterface, ScannerType } from "./ScannerInterface"; +import { NobleScanner } from "./noble/NobleScanner"; +import { TTBluetoothDevice } from "../device/TTBluetoothDevice"; +import { DeviceInterface } from "./DeviceInterface"; + +export { ScannerType } from "./ScannerInterface"; +export const TTLockUUIDs: string[] = ["1910", "00001910-0000-1000-8000-00805f9b34fb"]; + +export interface BluetoothLeService { + on(event: "ready", listener: () => void): this; + on(event: "discover", listener: (device: TTBluetoothDevice) => void): this; + on(event: "scanStart", listener: () => void): this; + on(event: "scanStop", listener: () => void): this; +} + +export class BluetoothLeService extends EventEmitter implements BluetoothLeService { + private scanner: ScannerInterface; + private devices: Map; + + constructor(uuids: string[] = TTLockUUIDs, scannerType: ScannerType = "auto") { + super(); + this.devices = new Map(); + if (scannerType == "auto") { + scannerType = "noble"; + } + if (scannerType == "noble") { + this.scanner = new NobleScanner(uuids); + this.scanner.on("ready", () => this.emit("ready")); + this.scanner.on("discover", this.onDiscover.bind(this)); + this.scanner.on("scanStart", () => this.emit("scanStart")); + this.scanner.on("scanStop", () => this.emit("scanStop")); + } else { + throw "Invalid parameters"; + } + } + + async startScan(): Promise { + return await this.scanner.startScan(); + } + + async stopScan(): Promise { + return await this.scanner.stopScan(); + } + + private onDiscover(btDevice: DeviceInterface) { + // TODO: move device storage to TTLockClient + // check if the device was previously discovered and update + if(this.devices.has(btDevice.id)) { + const device = this.devices.get(btDevice.id); + if (typeof device != 'undefined') { + device.updateFromDevice(btDevice); + // I don't think we should resend the discover on update + // this.emit("discover", device); + } + } else { + const device = TTBluetoothDevice.createFromDevice(btDevice); + this.devices.set(btDevice.id, device); + this.emit("discover", device); + } + } +} \ No newline at end of file diff --git a/src/scanner/DeviceInterface.ts b/src/scanner/DeviceInterface.ts new file mode 100644 index 0000000..8ea7dae --- /dev/null +++ b/src/scanner/DeviceInterface.ts @@ -0,0 +1,69 @@ +'use strict'; + +import { EventEmitter } from "events"; + +export interface DeviceInterface extends EventEmitter { + id: string; + uuid: string; + name: string; + address: string; + addressType: string; + connectable: boolean; + rssi: number; + mtu: number; + manufacturerData: Buffer; + services: Map; + busy: boolean; + checkBusy(): boolean; + resetBusy(): boolean; + connect(): Promise; + disconnect(): Promise; + discoverServices(): Promise>; + readCharacteristics(): Promise; + toJSON(asObject: boolean): string | Object; + toString(): string; + + on(event: "connected", listener: () => void): this; + on(event: "disconnected", listener: () => void): this; +} + +export interface ServiceInterface { + uuid: string; + name?: string; + type?: string; + includedServiceUuids: string[]; + characteristics: Map; + discoverCharacteristics(): Promise>; + readCharacteristics(): Promise> + toJSON(asObject: boolean): string | Object; + toString(): string; +} + +export interface CharacteristicInterface extends EventEmitter { + uuid: string; + name?: string; + type?: string; + properties: string[]; + isReading: boolean; + lastValue?: Buffer; + descriptors: Map; + discoverDescriptors(): Promise>; + read(): Promise; + write(data: Buffer, withoutResponse: boolean): Promise; + subscribe(): Promise; + toJSON(asObject: boolean): string | Object; + toString(): string; + + on(event: "dataRead", listener: (data: Buffer) => void): this; +} + +export interface DescriptorInterface { + uuid: string; + name?: string; + type?: string; + lastValue?: Buffer; + readValue(): Promise; + writeValue(data: Buffer): Promise; + toJSON(asObject: boolean): string | Object; + toString(): string; +} \ No newline at end of file diff --git a/src/scanner/ScannerInterface.ts b/src/scanner/ScannerInterface.ts new file mode 100644 index 0000000..4d9fd14 --- /dev/null +++ b/src/scanner/ScannerInterface.ts @@ -0,0 +1,18 @@ +'use strict'; + +import { EventEmitter } from "events"; +import { DeviceInterface } from "./DeviceInterface"; + +export type ScannerType = "noble" | "node-ble" | "auto"; + +export type ScannerStateType = "unknown" | "starting" | "scanning" | "stopping" | "stopped"; + +export interface ScannerInterface extends EventEmitter { + scannerState: ScannerStateType; + startScan(): Promise; + stopScan(): Promise; + on(event: "ready", listener: () => void): this; + on(event: "discover", listener: (device: DeviceInterface) => void): this; + on(event: "scanStart", listener: () => void): this; + on(event: "scanStop", listener: () => void): this; +} \ No newline at end of file diff --git a/src/scanner/noble/NobleCharacteristic.ts b/src/scanner/noble/NobleCharacteristic.ts new file mode 100644 index 0000000..d61228d --- /dev/null +++ b/src/scanner/noble/NobleCharacteristic.ts @@ -0,0 +1,116 @@ +'use strict'; + +import { Characteristic } from "@abandonware/noble"; +import { EventEmitter } from "events"; +import { CharacteristicInterface, DescriptorInterface } from "../DeviceInterface"; +import { NobleDescriptor } from "./NobleDescriptor"; +import { NobleDevice } from "./NobleDevice"; + +export class NobleCharacteristic extends EventEmitter implements CharacteristicInterface { + uuid: string; + name?: string | undefined; + type?: string | undefined; + properties: string[]; + isReading: boolean = false; + lastValue?: Buffer; + descriptors: Map = new Map(); + private device: NobleDevice; + private characteristic: Characteristic; + + constructor(device: NobleDevice, characteristic: Characteristic) { + super(); + this.device = device; + this.characteristic = characteristic; + this.uuid = characteristic.uuid; + this.name = characteristic.name; + this.type = characteristic.type; + this.properties = characteristic.properties; + this.characteristic.on("read", this.onRead.bind(this)); + } + + async discoverDescriptors(): Promise> { + this.device.checkBusy(); + if (!this.device.connected) { + this.device.resetBusy(); + throw new Error("NobleDevice is not connected"); + } + try { + const descriptors = await this.characteristic.discoverDescriptorsAsync(); + this.descriptors = new Map(); + descriptors.forEach((descriptor) => { + this.descriptors.set(descriptor.uuid, new NobleDescriptor(this.device, descriptor)); + }); + } catch (error) { + console.error(error); + } + this.device.resetBusy(); + return this.descriptors; + } + + async read(): Promise { + this.device.checkBusy(); + if (!this.device.connected) { + this.device.resetBusy(); + throw new Error("NobleDevice is not connected"); + } + this.isReading = true; + try { + this.lastValue = await this.characteristic.readAsync(); + } catch (error) { + console.error(error); + } + this.isReading = false; + this.device.resetBusy(); + return this.lastValue; + } + + async write(data: Buffer, withoutResponse: boolean): Promise { + this.device.checkBusy(); + if (!this.device.connected) { + this.device.resetBusy(); + throw new Error("NobleDevice is not connected"); + } + + await this.characteristic.writeAsync(data, withoutResponse); + + this.device.resetBusy(); + } + + async subscribe(): Promise { + await this.characteristic.subscribeAsync(); + // await this.characteristic.notifyAsync(true); + } + + private onRead(data: Buffer) { + // if the read notification comes from a manual read, just ignore it + // we are only interested in data pushed by the device + if (!this.isReading) { + this.lastValue = data; + this.emit("dataRead", this.lastValue); + } + } + + toJSON(asObject: boolean): string | Object { + let json: Record = { + uuid: this.uuid, + name: this.name, + type: this.type, + properties: this.properties, + value: this.lastValue?.toString("hex"), + descriptors: {} + } + this.descriptors.forEach((descriptor) => { + json.descriptors[this.uuid] = this.toJSON(true); + }); + + if (asObject) { + return json; + } else { + return JSON.stringify(json); + } + } + + toString(): string { + return this.characteristic.toString(); + } +} \ No newline at end of file diff --git a/src/scanner/noble/NobleDescriptor.ts b/src/scanner/noble/NobleDescriptor.ts new file mode 100644 index 0000000..41dcb79 --- /dev/null +++ b/src/scanner/noble/NobleDescriptor.ts @@ -0,0 +1,85 @@ +'use strict'; + +import { Descriptor } from "@abandonware/noble"; +import { EventEmitter } from "events"; +import { DescriptorInterface } from "../DeviceInterface"; +import { NobleDevice } from "./NobleDevice"; + +export class NobleDescriptor extends EventEmitter implements DescriptorInterface { + uuid: string; + name?: string | undefined; + type?: string | undefined; + isReading: boolean = false; + lastValue?: Buffer; + private device: NobleDevice; + private descriptor: Descriptor; + + constructor(device: NobleDevice, descriptor: Descriptor) { + super(); + this.device = device; + this.descriptor = descriptor; + this.uuid = descriptor.uuid; + this.name = descriptor.name; + this.type = descriptor.type; + this.descriptor.on("valueRead", this.onRead.bind(this)); + } + + async readValue(): Promise { + this.device.checkBusy(); + if (!this.device.connected) { + this.device.resetBusy(); + throw new Error("NobleDevice is not connected"); + } + this.isReading = true; + try { + this.lastValue = await this.descriptor.readValueAsync(); + } catch (error) { + console.error(error); + } + this.isReading = false; + this.device.resetBusy(); + return this.lastValue; + } + + async writeValue(data: Buffer): Promise { + this.device.checkBusy(); + if (!this.device.connected) { + this.device.resetBusy(); + throw new Error("NobleDevice is not connected"); + } + + await this.descriptor.writeValueAsync(data); + this.lastValue = data; + + this.device.resetBusy(); + } + + private onRead(data: Buffer) { + // if the read notification comes from a manual read, just ignore it + // we are only interested in data pushed by the device + if (!this.isReading) { + this.lastValue = data; + console.log("Descriptor received data", data); + this.emit("valueRead", this.lastValue); + } + } + + toJSON(asObject: boolean = false) { + const json = { + uuid: this.uuid, + name: this.name, + type: this.type, + value: this.lastValue?.toString("hex") + } + + if (asObject) { + return json; + } else { + return JSON.stringify(json); + } + } + + toString(): string { + return this.descriptor.toString(); + } +} \ No newline at end of file diff --git a/src/scanner/noble/NobleDevice.ts b/src/scanner/noble/NobleDevice.ts new file mode 100644 index 0000000..725d9e4 --- /dev/null +++ b/src/scanner/noble/NobleDevice.ts @@ -0,0 +1,173 @@ +'use strict'; + +import { DeviceInterface, ServiceInterface } from "../DeviceInterface"; +import { Peripheral } from "@abandonware/noble"; +import { EventEmitter } from "events"; +import { NobleService } from "./NobleService"; + +export class NobleDevice extends EventEmitter implements DeviceInterface { + id: string; + uuid: string; + name: string; + address: string; + addressType: string; + connectable: boolean; + connected: boolean = false; + rssi: number; + mtu: number = 20; + manufacturerData: Buffer; + services: Map; + busy: boolean = false; + private peripheral: Peripheral; + + constructor(peripheral: Peripheral) { + super(); + this.peripheral = peripheral; + this.id = peripheral.id; + this.uuid = peripheral.uuid; + this.name = peripheral.advertisement.localName; + this.address = peripheral.address.replace(/\-/g, ':'); + this.addressType = peripheral.addressType; + this.connectable = peripheral.connectable; + this.rssi = peripheral.rssi + // this.mtu = peripheral.mtu; + if (peripheral.advertisement.manufacturerData) { + this.manufacturerData = peripheral.advertisement.manufacturerData; + } else { + this.manufacturerData = Buffer.from([]); + } + this.peripheral.on("disconnect", this.onDisconnect.bind(this)); + this.services = new Map(); + } + + checkBusy(): boolean { + if (this.busy) { + throw new Error("NobleDevice is busy"); + } else { + this.busy = true; + return true; + } + } + + resetBusy(): boolean { + if (this.busy) { + this.busy = false; + } + return this.busy; + } + + async connect(): Promise { + if (this.connectable && !this.connected) { + await this.peripheral.connectAsync(); + this.connected = true; + this.emit("connected"); + return true; + } + return false; + } + + async disconnect(): Promise { + if (this.connectable && this.connected) { + try { + await this.peripheral.disconnectAsync(); + return true; + } catch (error) { + console.error(error); + return false; + } + } + return false; + } + + /** + * Discover all services, characteristics and descriptors + */ + async discoverServices(): Promise> { + try { + this.checkBusy(); + if (!this.connected) { + this.resetBusy(); + throw new Error("NobleDevice not connected"); + } + const snc = await this.peripheral.discoverAllServicesAndCharacteristicsAsync(); + this.resetBusy(); + this.services = new Map(); + snc.services.forEach((service) => { + const s = new NobleService(this, service); + this.services.set(s.uuid, s); + }); + return this.services; + } catch (error) { + console.error(error); + this.resetBusy(); + return new Map(); + } + } + + /** + * Read all available characteristics + */ + async readCharacteristics(): Promise { + try { + if (!this.connected) { + throw new Error("NobleDevice not connected"); + } + if (this.services.size == 0) { + await this.discoverServices(); + } + for (let [uuid, service] of this.services) { + for (let [uuid, characteristic] of service.characteristics) { + if (characteristic.properties.includes("read")) { + console.log("Reading", uuid); + const data = await characteristic.read(); + if (typeof data != "undefined") { + console.log("Data", data.toString("ascii")); + } + } + } + } + return true; + } catch (error) { + console.error(error); + return false; + } + } + + onDisconnect() { + this.connected = false; + this.services = new Map(); + this.emit("disconnected"); + } + + toString(): string { + let text = ""; + this.services.forEach((service) => { + text += service.toString() + "\n"; + }); + return text; + } + + toJSON(asObject: boolean = false): string | Object { + let json: Record = { + id: this.id, + uuid: this.uuid, + name: this.name, + address: this.address, + addressType: this.addressType, + connectable: this.connectable, + rssi: this.rssi, + mtu: this.mtu, + services: {} + }; + let services: Record = {} + this.services.forEach((service) => { + json.services[service.uuid] = service.toJSON(true); + }); + + if (asObject) { + return json; + } else { + return JSON.stringify(json); + } + } +} \ No newline at end of file diff --git a/src/scanner/noble/NobleScanner.ts b/src/scanner/noble/NobleScanner.ts new file mode 100644 index 0000000..dfb9fd4 --- /dev/null +++ b/src/scanner/noble/NobleScanner.ts @@ -0,0 +1,108 @@ +'use strict'; + +import { ScannerInterface, ScannerStateType } from "../ScannerInterface"; +import noble from "@abandonware/noble"; +import { EventEmitter } from "events"; +import { NobleDevice } from "./NobleDevice"; + +type nobleStateType = "unknown" | "resetting" | "unsupported" | "unauthorized" | "poweredOff" | "poweredOn"; + +export class NobleScanner extends EventEmitter implements ScannerInterface { + uuids: string[]; + scannerState: ScannerStateType = "unknown"; + private nobleState: nobleStateType = "unknown"; + private devices: Set = new Set(); + + constructor(uuids: string[] = []) { + super(); + this.uuids = uuids; + noble.on('discover', this.onNobleDiscover.bind(this)); + noble.on('stateChange', this.onNobleStateChange.bind(this)); + noble.on('scanStart', this.onNobleScanStart.bind(this)); + noble.on('scanStop', this.onNobleScanStop.bind(this)); + } + + getState(): ScannerStateType { + return this.scannerState; + } + + async startScan(): Promise { + if (this.scannerState == "unknown" || this.scannerState == "stopped") { + if (this.nobleState == "poweredOn") { + this.scannerState = "starting"; + return await this.startNobleScan(); + } else { + return false; + } + } + return false; + } + + async stopScan(): Promise { + if (this.scannerState == "scanning") { + this.scannerState = "stopping"; + return await this.stopNobleScan(); + } + return false; + } + + private async startNobleScan(): Promise { + try { + await noble.startScanningAsync(this.uuids, false); + this.scannerState = "scanning"; + return true; + } catch (error) { + console.error(error); + if (this.scannerState == "starting") { + this.scannerState = "stopped"; + } + return false; + } + } + + private async stopNobleScan(): Promise { + try { + await noble.stopScanningAsync(); + this.scannerState = "stopped"; + return true; + } catch (error) { + console.error(error); + if (this.scannerState == "stopping") { + this.scannerState = "scanning"; + } + return false; + } + } + + private onNobleStateChange(state: nobleStateType): void { + this.nobleState = state; + if (this.nobleState == "poweredOn") { + this.emit("ready"); + } + if (this.scannerState == "starting" && this.nobleState == "poweredOn") { + this.startNobleScan(); + } + } + + private async onNobleDiscover(peripheral: noble.Peripheral): Promise { + // if the device was already found, maybe advertisement has changed + if (!this.devices.has(peripheral.id)) { + this.devices.add(peripheral.id); + // first time found, scan the services + const nobleDevice = new NobleDevice(peripheral); + // await nobleDevice.discoverServices(); + this.emit("discover", nobleDevice); + } + } + + private onNobleScanStart(): void { + this.scannerState = "scanning"; + this.emit("scanStart"); + } + + private onNobleScanStop(): void { + this.scannerState = "stopped"; + this.emit("scanStop"); + } +} + diff --git a/src/scanner/noble/NobleService.ts b/src/scanner/noble/NobleService.ts new file mode 100644 index 0000000..36e2c06 --- /dev/null +++ b/src/scanner/noble/NobleService.ts @@ -0,0 +1,85 @@ +'use strict'; + +import { Service } from "@abandonware/noble"; +import { CharacteristicInterface, ServiceInterface } from "../DeviceInterface"; +import { NobleCharacteristic } from "./NobleCharacteristic"; +import { NobleDevice } from "./NobleDevice"; + +export class NobleService implements ServiceInterface { + uuid: string; + name: string; + type: string; + includedServiceUuids: string[]; + characteristics: Map = new Map(); + private device: NobleDevice; + private service: Service; + + constructor(device: NobleDevice, service: Service) { + this.device = device; + this.service = service; + this.uuid = service.uuid; + this.name = service.name; + this.type = service.type; + this.includedServiceUuids = service.includedServiceUuids; + // also add characteristics if they exist + if (service.characteristics && service.characteristics.length > 0) { + this.characteristics = new Map(); + service.characteristics.forEach((characteristic) => { + const c = new NobleCharacteristic(this.device, characteristic); + this.characteristics.set(c.uuid, c); + }); + } + } + + async discoverCharacteristics(): Promise> { + try { + this.device.checkBusy(); + const characteristics = await this.service.discoverCharacteristicsAsync(); + this.device.resetBusy(); + this.characteristics = new Map(); + characteristics.forEach((characteristic) => { + const c = new NobleCharacteristic(this.device, characteristic); + this.characteristics.set(c.uuid, c); + }); + return this.characteristics; + } catch (error) { + console.error(error); + this.device.resetBusy(); + return new Map(); + } + } + + async readCharacteristics(): Promise> { + if (this.characteristics.size == 0) { + await this.discoverCharacteristics(); + } + + for (let [uuid, characteristic] of this.characteristics) { + await characteristic.read(); + } + + return this.characteristics; + } + + toJSON(asObject: boolean): string | Object { + let json: Record = { + uuid: this.uuid, + name: this.name, + type: this.type, + characteristics: {} + }; + this.characteristics.forEach((characteristic) => { + json.characteristics[characteristic.uuid] = characteristic.toJSON(true); + }); + + if (asObject) { + return json; + } else { + return JSON.stringify(json); + } + } + + toString(): string { + return this.service.toString(); + } +} \ No newline at end of file diff --git a/src/store/TTLockData.ts b/src/store/TTLockData.ts new file mode 100644 index 0000000..ec04d0a --- /dev/null +++ b/src/store/TTLockData.ts @@ -0,0 +1,26 @@ +'use strict'; + +import { CodeSecret } from "../api/Commands/InitPasswordsCommand"; +import { AdminType } from "../device/AdminType"; + +export interface TTLockPrivateData { + aesKey?: string; + admin?: AdminType; + adminPasscode?: string; + pwdInfo?: CodeSecret[]; +} + +export interface TTLockData { + /** MAC address */ + address: string; + /** Battery level */ + battery: number; + /** Signal */ + rssi: number; + /** Auto lock time in seconds */ + autoLockTime: number; + /** -1 unknown, 0 locked, 1 unlocked */ + lockedStatus: number; + /** Lock private data */ + privateData: TTLockPrivateData; +} \ No newline at end of file diff --git a/src/util/AESUtil.ts b/src/util/AESUtil.ts new file mode 100644 index 0000000..abf0538 --- /dev/null +++ b/src/util/AESUtil.ts @@ -0,0 +1,62 @@ +'use strict'; + +import crypto from "crypto"; + +/** + * Default encryption key used when the lock is not paired yet + */ +export const defaultAESKey = Buffer.from([ + 0x98, 0x76, 0x23, 0xE8, + 0xA9, 0x23, 0xA1, 0xBB, + 0x3D, 0x9E, 0x7D, 0x03, + 0x78, 0x12, 0x45, 0x88 +]); + +export class AESUtil { + static aesEncrypt(source: Buffer, key?: Buffer): Buffer { + if (source.length == 0) { + return Buffer.from([]); + } + + if (typeof key == "undefined") { + key = defaultAESKey; + } + + if (key.length != 16) { + throw new Error("Invalid key size: " + key.length); + } + + const cipher = crypto.createCipheriv('aes-128-cbc', key, key); + + let encrypted = cipher.update(source); + encrypted = Buffer.concat([encrypted, cipher.final()]); + + return encrypted; + } + + static aesDecrypt(source: Buffer, key?: Buffer): Buffer { + if (source.length == 0) { + return Buffer.from([]); + } + + if (typeof key == "undefined") { + key = defaultAESKey; + } + + if (key.length != 16) { + throw new Error("Invalid key size: " + key.length); + } + + const cipher = crypto.createDecipheriv('aes-128-cbc', key, key); + + try { + let decrypted = cipher.update(source); + decrypted = Buffer.concat([decrypted, cipher.final()]); + + return decrypted; + } catch (error) { + console.error(error); + throw new Error("Decryption failed"); + } + } +} \ No newline at end of file diff --git a/src/util/CodecUtils.ts b/src/util/CodecUtils.ts new file mode 100644 index 0000000..67b410b --- /dev/null +++ b/src/util/CodecUtils.ts @@ -0,0 +1,63 @@ +'use strict'; + +/** TODO: use Buffers */ + +import { dscrc_table } from "./dscrc_table"; + +export class CodecUtils { + static encodeWithEncrypt(p0: Buffer, key?: number): Buffer { + var seed; + if (key) { + seed = key; + } else { + // generate a random number from 1 to 127 + seed = Math.round(Math.random() * 126) + 1; + } + + var encoded = []; + const crc = dscrc_table[p0.length & 0xff]; + + for (var i = 0; i < p0.length; i++) { + encoded.push(seed ^ p0.readInt8(i) ^ crc); + } + if (!key) { + encoded.push(seed); + } + + return Buffer.from(encoded); + } + + static encode(p0: Buffer): Buffer { + return CodecUtils.encodeWithEncrypt(p0); + } + + static decodeWithEncrypt(p0: Buffer, key?: number): Buffer { + var seed; + if (key) { + seed = key; + } else { + seed = p0.readInt8(p0.length - 1); + } + + var decoded = []; + const crc = dscrc_table[p0.length & 0xff]; + + for (var i = 0; i < p0.length - (key ? 0 : 1); i++) { + decoded.push(seed ^ p0[i] ^ crc); + } + + return Buffer.from(decoded); + } + + static decode(p0: Buffer): Buffer { + return CodecUtils.decodeWithEncrypt(p0); + } + + static crccompute(p0: Buffer): number { + var crc = 0; + for (var i = 0; i < p0.length; i++) { + crc = dscrc_table[crc ^ p0.readUInt8(i)]; + } + return crc; + } +} diff --git a/src/util/digitUtil.ts b/src/util/digitUtil.ts new file mode 100644 index 0000000..51d20db --- /dev/null +++ b/src/util/digitUtil.ts @@ -0,0 +1,9 @@ +'use strict'; + +export function padHexString(s: string): string { + if (s.length % 2 != 0) { + return "0" + s; + } else { + return s; + } +} \ No newline at end of file diff --git a/src/util/dscrc_table.ts b/src/util/dscrc_table.ts new file mode 100644 index 0000000..dd191b8 --- /dev/null +++ b/src/util/dscrc_table.ts @@ -0,0 +1,20 @@ +'use strict'; + +export const dscrc_table: number[] = [ + 0, 94,188,226, 97, 63,221,131,194,156,126, 32,163,253, 31, 65, +157,195, 33,127,252,162, 64, 30, 95, 1,227,189, 62, 96,130,220, + 35,125,159,193, 66, 28,254,160,225,191, 93, 3,128,222, 60, 98, +190,224, 2, 92,223,129, 99, 61,124, 34,192,158, 29, 67,161,255, + 70, 24,250,164, 39,121,155,197,132,218, 56,102,229,187, 89, 7, +219,133,103, 57,186,228, 6, 88, 25, 71,165,251,120, 38,196,154, +101, 59,217,135, 4, 90,184,230,167,249, 27, 69,198,152,122, 36, +248,166, 68, 26,153,199, 37,123, 58,100,134,216, 91, 5,231,185, +140,210, 48,110,237,179, 81, 15, 78, 16,242,172, 47,113,147,205, + 17, 79,173,243,112, 46,204,146,211,141,111, 49,178,236, 14, 80, +175,241, 19, 77,206,144,114, 44,109, 51,209,143, 12, 82,176,238, + 50,108,142,208, 83, 13,239,177,240,174, 76, 18,145,207, 45,115, +202,148,118, 40,171,245, 23, 73, 8, 86,180,234,105, 55,213,139, + 87, 9,235,181, 54,104,138,212,149,203, 41,119,244,170, 72, 22, +233,183, 85, 11,136,214, 52,106, 43,117,151,201, 74, 20,246,168, +116, 42,200,150, 21, 75,169,247,182,232, 10, 84,215,137,107, 53 +]; diff --git a/src/util/jsonUtil.ts b/src/util/jsonUtil.ts new file mode 100644 index 0000000..06fa2e7 --- /dev/null +++ b/src/util/jsonUtil.ts @@ -0,0 +1,22 @@ +'use strict'; + +/** + * Recursively converts all buffers to hex string in an object + * @param json Object to convert Buffers to string + */ +export function stringifyBuffers(json: Object): Object { + if((json instanceof Set) || (json instanceof Map)) { + return json; + } + Object.getOwnPropertyNames(json).forEach((key) => { + const val = Reflect.get(json, key); + if (typeof val == "object" && val instanceof Buffer) { + Reflect.set(json, key, val.toString('hex')); + } else if (typeof val == "object") { + Reflect.set(json, key, stringifyBuffers(val)); + } else { + // Reflect.set(json, key, val); + } + }); + return json; +} \ No newline at end of file diff --git a/src/util/timeUtil.ts b/src/util/timeUtil.ts new file mode 100644 index 0000000..01d2e4d --- /dev/null +++ b/src/util/timeUtil.ts @@ -0,0 +1,9 @@ +'use strict'; + +export function dateTimeToBuffer(dateTime: string): Buffer { + const result = Buffer.alloc(dateTime.length/2); + for (let i = 0; i < result.length; i++) { + result[i] = parseInt(dateTime.substring(i * 2, i * 2 + 2)); + } + return result; +} \ No newline at end of file diff --git a/src/util/timingUtil.ts b/src/util/timingUtil.ts new file mode 100644 index 0000000..62f031d --- /dev/null +++ b/src/util/timingUtil.ts @@ -0,0 +1,11 @@ +'use strict'; + +/** + * Sleep for + * @param ms miliseconds + */ +export function sleep(ms: number): Promise { + return new Promise((resolve) => { + setTimeout(resolve, ms) + }); +} \ No newline at end of file diff --git a/tools/debug.js b/tools/debug.js new file mode 100644 index 0000000..a3011b0 --- /dev/null +++ b/tools/debug.js @@ -0,0 +1,108 @@ +'use strict'; + +const { TTLockClient, sleep, CommandEnvelope } = require('../dist'); + +// const uuids = []; +const api = new TTLockClient({ + // uuids: uuids, + // scannerType: "noble" +}); + +async function doStuff() { + api.prepareBTService(); + for (let i = 10; i > 0; i--) { + console.log("Starting scan...", i); + await sleep(1000); + } + api.startScanLock(); + console.log("Scan started"); + api.on("foundLock", async (device) => { + await device.connect(); + // don't disconnect so we receive subscriptions + // await device.disconnect(); + console.log(device.toJSON()); + console.log(); + + if (!device.isInitialized()) { + console.log("Trying to init the lock"); + console.log(); + console.log(); + const inited = await device.initLock(); + if (!inited) { + process.exit(1); + } + await device.disconnect(); + console.log(); + console.log(); + console.log(device.toJSON()); + console.log(); + console.log(); + console.log("Sleeping 10 seconds"); + await sleep(10000); + console.log("Reseting to factory defaults"); + await device.connect(); + console.log(); + console.log(); + await device.resetLock(); + console.log(); + console.log(); + console.log("Lock reset to factory defaults"); + process.exit(0); + } + }); +} + +// doStuff(); + +let defaultAes = Buffer.from("987623E8A923A1BB3D9E7D0378124588", "hex"); +let aes = Buffer.from("e817e962c7176c296403f646129f362c", "hex"); +// let sent = Buffer.from("7f5a0503010001000190aa108419ca5d7ddc8fa963e1118cacf6f26b27", "hex"); +let received = Buffer.from("7f5a0503020001000154aa1095a3bd4703fde2b76397587b6ee44b7b28", "hex"); +let receivedCommand = CommandEnvelope.createFromRawData(received, aes); +// receivedCommand.buildCommandBuffer(); +let cmd = receivedCommand.getCommand(); +// console.log(cmd.getData()); +console.log(receivedCommand); +console.log(receivedCommand.getCommandType().toString(16), String.fromCharCode(receivedCommand.getCommandType())); +console.log(receivedCommand.getData().toString("hex")); +console.log(cmd.getType()); +// console.log(cmd); +// console.log(cmd.getPsFromLock()); +// let cmd = commandFromData(receivedCommand.getData()); + + +let data = receivedCommand.getData(); +// let data = Buffer.from("140ed0cf86ee2d2d70dd779a28060154283feb3941263687ae30417a9318e137103988e11a13110e0d48f20f013685f67c23d064c2943815e06e8c27ef", "hex"); + +//////////////////////// +// InitPasswordsCommand +// console.log("data length:", data.length); +// console.log("year:", data.readUInt8(0)); +// for (let i = 0; i < 10; i++) { +// const codeSecret = data.subarray(i * 6 + 1, i * 6 + 1 + 6); +// // console.log(codeSecret); +// let code = 0; +// code = codeSecret.readUInt16BE(0) >> 4 & 0xFFFF; +// // code = codeSecret.readUInt8(0) << 4 & 0xFF; +// // code = code | (codeSecret.readUInt8(1) >> 4); +// let sec = Buffer.alloc(8); +// sec[3] = (codeSecret.readUInt8(1) << 4 & 0xFF) >> 4; +// codeSecret.copy(sec, 4, 2); +// console.log("Code:",code, "Secret:", sec.readBigUInt64BE(0).toString()); +// } + + +// console.log(data.toString()); + +// for (let i = 0; i < 10; i++) { +// console.log(data.readUInt8(i)); +// } +// console.log(data.readUInt32BE(0), data.readUInt32BE(4), data.subarray(8).toString()); +// console.log(data.toString()); + +// console.log(data.subarray(0, 5)); +// console.log(data.subarray(5, 5)); +// const data2 = Buffer.alloc(4); +// data.copy(data2, 1, 10, 13); +// console.log(data2.readUInt32BE(0)); +// console.log(data.readUInt32BE(0)); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..1310c55 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,71 @@ +{ + "extends": "@tsconfig/node12/tsconfig.json", + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + // "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ + // "module": "system", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + // "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./dist/ttlock-sdk.js", /* Concatenate and emit output to single file. */ + "outDir": "./dist", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + // "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + // "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + // "skipLibCheck": true, /* Skip type checking of declaration files. */ + // "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + }, + "include": ["src/**/*"] +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..0a27bf7 --- /dev/null +++ b/tslint.json @@ -0,0 +1,11 @@ +{ + "defaultSeverity": "error", + "extends": [ + "tslint:recommended" + ], + "jsRules": {}, + "rules": { + "no-console": false + }, + "rulesDirectory": [] +} \ No newline at end of file