From c1cc2895e5a6754239990820355b60b155117f7d Mon Sep 17 00:00:00 2001 From: babayaga Date: Fri, 23 May 2025 09:36:43 +0200 Subject: [PATCH] License & Components - Anne & the Gang --- LICENSE-MODEL.txt | 83 +++ LICENSE.txt | 674 +++++++++++++++++++ README.md | 40 +- src/components/3PosAnalog.h | 287 ++++++++ src/components/AnalogLevelSwitch.cpp | 324 +++++++++ src/components/AnalogLevelSwitch.h | 224 +++++++ src/components/Extruder.cpp | 517 +++++++++++++++ src/components/Extruder.h | 124 ++++ src/components/GPIO.h | 572 ++++++++++++++++ src/components/Joystick.cpp | 270 ++++++++ src/components/Joystick.h | 104 +++ src/components/LEDFeedback.cpp | 339 ++++++++++ src/components/LEDFeedback.h | 110 ++++ src/components/ModbusLogicEngine.cpp | 417 ++++++++++++ src/components/ModbusLogicEngine.h | 200 ++++++ src/components/OmronE5.cpp | 840 ++++++++++++++++++++++++ src/components/OmronE5.h | 167 +++++ src/components/OmronE5Types.h | 361 +++++++++++ src/components/OmronE5_Ex.h | 260 ++++++++ src/components/POT.h | 346 ++++++++++ src/components/Plunger.cpp | 759 ++++++++++++++++++++++ src/components/Plunger.h | 247 +++++++ src/components/PlungerModbus.cpp | 117 ++++ src/components/PlungerSettings.cpp | 213 ++++++ src/components/PlungerSettings.h | 107 +++ src/components/PlungerStates.cpp | 559 ++++++++++++++++ src/components/Relay.h | 180 +++++ src/components/SAKO_VFD.cpp | 723 +++++++++++++++++++++ src/components/SAKO_VFD.h | 147 +++++ src/components/Sako-Registers.h | 937 +++++++++++++++++++++++++++ src/components/SakoTypes.h | 122 ++++ src/components/StatusLight.h | 201 ++++++ src/components/StepperController.h | 174 +++++ 33 files changed, 10708 insertions(+), 37 deletions(-) create mode 100644 LICENSE-MODEL.txt create mode 100644 LICENSE.txt create mode 100644 src/components/3PosAnalog.h create mode 100644 src/components/AnalogLevelSwitch.cpp create mode 100644 src/components/AnalogLevelSwitch.h create mode 100644 src/components/Extruder.cpp create mode 100644 src/components/Extruder.h create mode 100644 src/components/GPIO.h create mode 100644 src/components/Joystick.cpp create mode 100644 src/components/Joystick.h create mode 100644 src/components/LEDFeedback.cpp create mode 100644 src/components/LEDFeedback.h create mode 100644 src/components/ModbusLogicEngine.cpp create mode 100644 src/components/ModbusLogicEngine.h create mode 100644 src/components/OmronE5.cpp create mode 100644 src/components/OmronE5.h create mode 100644 src/components/OmronE5Types.h create mode 100644 src/components/OmronE5_Ex.h create mode 100644 src/components/POT.h create mode 100644 src/components/Plunger.cpp create mode 100644 src/components/Plunger.h create mode 100644 src/components/PlungerModbus.cpp create mode 100644 src/components/PlungerSettings.cpp create mode 100644 src/components/PlungerSettings.h create mode 100644 src/components/PlungerStates.cpp create mode 100644 src/components/Relay.h create mode 100644 src/components/SAKO_VFD.cpp create mode 100644 src/components/SAKO_VFD.h create mode 100644 src/components/Sako-Registers.h create mode 100644 src/components/SakoTypes.h create mode 100644 src/components/StatusLight.h create mode 100644 src/components/StepperController.h diff --git a/LICENSE-MODEL.txt b/LICENSE-MODEL.txt new file mode 100644 index 00000000..bd97962a --- /dev/null +++ b/LICENSE-MODEL.txt @@ -0,0 +1,83 @@ +POLYMECH LICENSE AGREEMENT + +Version 1.0, 23 October 2025 + +Copyright (c) 2025 POLYMECH + +Section I: PREAMBLE + + +This License governs the use of the model (and its derivatives) and is informed by the model card associated with the model. + +NOW THEREFORE, You and POLYMECH agree as follows: + +1. Definitions +"License" means the terms and conditions for use, reproduction, and Distribution as defined in this document. +"Data" means a collection of information and/or content extracted from the dataset used with the Model, including to train, pretrain, or otherwise evaluate the Model. The Data is not licensed under this License. +"Model" means any accompanying machine-learning based assemblies (including checkpoints), consisting of learnt weights, parameters (including optimizer states), corresponding to the model architecture as embodied in the Complementary Material, that have been trained or tuned, in whole or in part on the Data, using the Complementary Material. +"Derivatives of the Model" means all modifications to the Model, works based on the Model, or any other model which is created or initialized by transfer of patterns of the weights, parameters, activations or output of the Model, to the other model, in order to cause the other model to perform similarly to the Model, including - but not limited to - distillation methods entailing the use of intermediate data representations or methods based on the generation of synthetic data by the Model for training the other model. +"Complementary Material" means the accompanying source code and scripts used to define, run, load, benchmark or evaluate the Model, and used to prepare data for training or evaluation, if any. This includes any accompanying documentation, tutorials, examples, etc, if any. +"Distribution" means any transmission, reproduction, publication or other sharing of the Model or Derivatives of the Model to a third party, including providing the Model as a hosted service made available by electronic or other remote means - e.g. API-based or web access. +"POLYMECH" (or "we") means Beijing POLYMECH Artificial Intelligence Fundamental Technology Research Co., Ltd., Hangzhou POLYMECH Artificial Intelligence Fundamental Technology Research Co., Ltd. and/or any of their affiliates. +"You" (or "Your") means an individual or Legal Entity exercising permissions granted by this License and/or making use of the Model for whichever purpose and in any field of use, including usage of the Model in an end-use application - e.g. chatbot, translator, etc. +"Third Parties" means individuals or legal entities that are not under common control with POLYMECH or You. + +Section II: INTELLECTUAL PROPERTY RIGHTS + +Both copyright and patent grants apply to the Model, Derivatives of the Model and Complementary Material. The Model and Derivatives of the Model are subject to additional terms as described in Section III. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, POLYMECH hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare, publicly display, publicly perform, sublicense, and distribute the Complementary Material, the Model, and Derivatives of the Model. + +3. Grant of Patent License. Subject to the terms and conditions of this License and where and as applicable, POLYMECH hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this paragraph) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Model and the Complementary Material, where such license applies only to those patent claims licensable by POLYMECH that are necessarily infringed by its contribution(s). If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Model and/or Complementary Material constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for the Model and/or works shall terminate as of the date such litigation is asserted or filed. + + +Section III: CONDITIONS OF USAGE, DISTRIBUTION AND REDISTRIBUTION + +4. Distribution and Redistribution. You may host for Third Party remote access purposes (e.g. software-as-a-service), reproduce and distribute copies of the Model or Derivatives of the Model thereof in any medium, with or without modifications, provided that You meet the following conditions: +a. Use-based restrictions as referenced in paragraph 5 MUST be included as an enforceable provision by You in any type of legal agreement (e.g. a license) governing the use and/or distribution of the Model or Derivatives of the Model, and You shall give notice to subsequent users You Distribute to, that the Model or Derivatives of the Model are subject to paragraph 5. This provision does not apply to the use of Complementary Material. +b. You must give any Third Party recipients of the Model or Derivatives of the Model a copy of this License; +c. You must cause any modified files to carry prominent notices stating that You changed the files; +d. You must retain all copyright, patent, t1rademark, and attribution notices excluding those notices that do not pertain to any part of the Model, Derivatives of the Model. +e. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions - respecting paragraph 4.a. – for use, reproduction, or Distribution of Your modifications, or for any such Derivatives of the Model as a whole, provided Your use, reproduction, and Distribution of the Model otherwise complies with the conditions stated in this License. + +5. Use-based restrictions. The restrictions set forth in Attachment A are considered Use-based restrictions. Therefore You cannot use the Model and the Derivatives of the Model for the specified restricted uses. You may use the Model subject to this License, including only for lawful purposes and in accordance with the License. Use may include creating any content with, finetuning, updating, running, training, evaluating and/or reparametrizing the Model. You shall require all of Your users who use the Model or a Derivative of the Model to comply with the terms of this paragraph (paragraph 5). + +6. The Output You Generate. Except as set forth herein, POLYMECH claims no rights in the Output You generate using the Model. You are accountable for the Output you generate and its subsequent uses. No use of the output can contravene any provision as stated in the License. + +Section IV: OTHER PROVISIONS + +7. Updates and Runtime Restrictions. To the maximum extent permitted by law, POLYMECH reserves the right to restrict (remotely or otherwise) usage of the Model in violation of this License. + +8. Trademarks and related. Nothing in this License permits You to make use of POLYMECH’ trademarks, trade names, logos or to otherwise suggest endorsement or misrepresent the relationship between the parties; and any rights not expressly granted herein are reserved by POLYMECH. + +9. Personal information, IP rights and related. This Model may contain personal information and works with IP rights. You commit to complying with applicable laws and regulations in the handling of personal information and the use of such works. Please note that POLYMECH's license granted to you to use the Model does not imply that you have obtained a legitimate basis for processing the related information or works. As an independent personal information processor and IP rights user, you need to ensure full compliance with relevant legal and regulatory requirements when handling personal information and works with IP rights that may be contained in the Model, and are willing to assume solely any risks and consequences that may arise from that. + +10. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, POLYMECH provides the Model and the Complementary Material on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Model, Derivatives of the Model, and the Complementary Material and assume any risks associated with Your exercise of permissions under this License. + +11. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall POLYMECH be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Model and the Complementary Material (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if POLYMECH has been advised of the possibility of such damages. + +12. Accepting Warranty or Additional Liability. While redistributing the Model, Derivatives of the Model and the Complementary Material thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of POLYMECH, and only if You agree to indemnify, defend, and hold POLYMECH harmless for any liability incurred by, or claims asserted against, POLYMECH by reason of your accepting any such warranty or additional liability. + +13. If any provision of this License is held to be invalid, illegal or unenforceable, the remaining provisions shall be unaffected thereby and remain valid as if such provision had not been set forth herein. + +14. Governing Law and Jurisdiction. This agreement will be governed and construed under PRC laws without regard to choice of law principles, and the UN Convention on Contracts for the International Sale of Goods does not apply to this agreement. The courts located in the domicile of Hangzhou POLYMECH Artificial Intelligence Fundamental Technology Research Co., Ltd. shall have exclusive jurisdiction of any dispute arising out of this agreement. + +END OF TERMS AND CONDITIONS + +Attachment A + +Use Restrictions + +You agree not to use the Model or Derivatives of the Model: + +- In any way that violates any applicable national or international law or regulation or infringes upon the lawful rights and interests of any third party; +- For military use in any way; +- For the purpose of exploiting, harming or attempting to exploit or harm minors in any way; +- To generate or disseminate verifiably false information and/or content with the purpose of harming others; +- To generate or disseminate inappropriate content subject to applicable regulatory requirements; +- To generate or disseminate personal identifiable information without due authorization or for unreasonable use; +- To defame, disparage or otherwise harass others; +- For fully automated decision making that adversely impacts an individual’s legal rights or otherwise creates or modifies a binding, enforceable obligation; +- For any use intended to or which has the effect of discriminating against or harming individuals or groups based on online or offline social behavior or known or predicted personal or personality characteristics; +- To exploit any of the vulnerabilities of a specific group of persons based on their age, social, physical or mental characteristics, in order to materially distort the behavior of a person pertaining to that group in a manner that causes or is likely to cause that person or another person physical or psychological harm; +- For any use intended to or which has the effect of discriminating against individuals or groups based on legally protected characteristics or categories. diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..f288702d --- /dev/null +++ b/LICENSE.txt @@ -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 index 4e0bb6d5..6e91ae94 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,6 @@ +# PolyMech Firmware Library -# OSR Firmware Library +## License -## Logging +We have added supplementary usage terms (see [./LICENSE-MODEL.txt](./LICENSE-MODEL.txt)) that explicitly prohibit employing this work to harm others. In particular, the terms forbid its use for brainwashing or profit-driven indoctrination, such as the “Precious Plastic” scam that targets young people (see [https://forum.osr-plastic.org/t/preciousplastic-review/11066](https://forum.osr-plastic.org/t/preciousplastic-review/11066)). -- [x] Serial ('Arduino-Log') -- [ ] Transport : TCP -- [ ] Flags -- [ ] Template (Plotter) - -## Compononents - -- [ ] Run-Time Flags -- [ ] Network Flags -- [ ] Variable Calling Convention -- [ ] Callback Signature Register -- [ ] Lifecycle: Start, Stop, Reset, Remove, Add -- [ ] Name: Optional -- [ ] Linked List -- [ ] Error Codes - -## Debugging - -## Networking - -- [ ] RS485 Proxy -- [ ] CAN -- [ ] TCP Raw -- [ ] BL - -## Compiling - -- [ ] Platform.IO Module -- [ ] Arduino-Lib Release - -## Documentation - -## References - -- [CPP Commons - CppPotpourri](https://github.com/jspark311/CppPotpourri.git) diff --git a/src/components/3PosAnalog.h b/src/components/3PosAnalog.h new file mode 100644 index 00000000..306a3242 --- /dev/null +++ b/src/components/3PosAnalog.h @@ -0,0 +1,287 @@ +#ifndef POS3_ANALOG_H +#define POS3_ANALOG_H + +#include +#include "../config.h" +#include +#include +#include +#include "modbus/ModbusTCP.h" +#include "config-modbus.h" +#include "enums.h" + +// Added POTControlMode enum definition here +enum class POTControlMode : uint8_t +{ + LOCAL = 0, + REMOTE = 1 +}; + +class Bridge; + +class Pos3Analog : public Component +{ +private: + const short modbusAddress; + POTControlMode controlMode; + int remoteValue; + + // Instance-specific storage for Modbus block definitions + MB_Registers m_modbus_blocks[3]; + // m_modbus_view needs to be mutable to be returned as ModbusBlockView* from a const method. + mutable ModbusBlockView m_modbus_view; + +public: + enum E_POS3_DIRECTION + { + UP = 1, + MIDDLE = 0, + DOWN = 2, + INVALID = 10 + }; + + Pos3Analog( + Component *owner, + short _upPin, short _downPin, + short _id, + short _modbusAddress) : Component("Pos3Analog", _id, Component::COMPONENT_DEFAULT, owner), + upPin(_upPin), + downPin(_downPin), + modbusAddress(_modbusAddress), + value(MIDDLE) + { + setNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS); + controlMode = POTControlMode::LOCAL; + remoteValue = E_POS3_DIRECTION::MIDDLE; + + // Initialize instance-specific Modbus blocks by direct struct initialization + // This matches the 8-field structure from the original static initialization. + + // Block for "3PosAnalog Value" + m_modbus_blocks[0] = { + static_cast(this->modbusAddress + 0), // startAddress + 1, // count + E_FN_CODE::FN_READ_HOLD_REGISTER, // type + MB_ACCESS_READ_ONLY, // access + static_cast(this->id), // field 5 (was component id) + 0, // field 6 (was offset 0) + "3PosAnalog Value", // name + "Pos3" // group + }; + + // Block for "3PosAnalog Mode" + m_modbus_blocks[1] = { + static_cast(this->modbusAddress + 1), // startAddress + 1, // count + E_FN_CODE::FN_READ_HOLD_REGISTER, // type + MB_ACCESS_READ_WRITE, // access + static_cast(this->id), // field 5 (was component id) + 1, // field 6 (was offset 1) + "3PosAnalog Mode (0=L,1=R)", // name + "Pos3" // group + }; + + // Block for "3PosAnalog Remote Value" + m_modbus_blocks[2] = { + static_cast(this->modbusAddress + 2), // startAddress + 1, // count + E_FN_CODE::FN_READ_HOLD_REGISTER, // type + MB_ACCESS_READ_WRITE, // access + static_cast(this->id), // field 5 (was component id) + 2, // field 6 (was offset 2) + "3PosAnalog Remote Value", // name + "Pos3" // group + }; + + // Initialize the view to point to these instance-specific blocks + m_modbus_view.data = m_modbus_blocks; + m_modbus_view.count = sizeof(m_modbus_blocks) / sizeof(m_modbus_blocks[0]); + } + + short setup() override + { + Component::setup(); + pinMode(upPin, INPUT); + pinMode(downPin, INPUT); + loop(); + return E_OK; + } + + short info(short val0 = 0, short val1 = 0) override + { + Log.verboseln("3PosAnalog::info - ID: %d, UpPin: %d, DownPin: %d, Modbus Addr: %d, Value: %d, NetCaps: %d, Mode: %d, RemoteVal: %d", + id, upPin, downPin, modbusAddress, value, nFlags, + static_cast(controlMode), remoteValue); + return E_OK; + } + short debug() override + { + return info(0, 0); + } + + short loop() override + { + Component::loop(); + if (now - last < ANALOG_SWITCH_READ_INTERVAL) + { + return E_OK; + } + last = now; + int newValue = value; + int readValue = E_POS3_DIRECTION::MIDDLE; + + if (controlMode == POTControlMode::LOCAL) + { + readValue = read(); + if (readValue != value) + { + newValue = readValue; + } + } + else + { + if (remoteValue != value) { + newValue = remoteValue; + } + } + + if (newValue != value) + { + value = newValue; + notifyStateChange(); + } + return E_OK; + } + + int getValue() const + { + return value; + } + + short mb_tcp_write(MB_Registers *reg, short networkValue) override + { + uint16_t addr = reg->startAddress; + bool changed = false; + + if (addr == modbusAddress + 1) + { + POTControlMode newMode = (networkValue == 0) ? POTControlMode::LOCAL : POTControlMode::REMOTE; + if (newMode != controlMode) + { + Log.verboseln("3PosAnalog::mb_write - ID:%d Mode change %d -> %d", id, static_cast(controlMode), static_cast(newMode)); + controlMode = newMode; + changed = true; + } + } + else if (addr == modbusAddress + 2) + { + int clampedValue = networkValue; + if (clampedValue != E_POS3_DIRECTION::MIDDLE && + clampedValue != E_POS3_DIRECTION::UP && + clampedValue != E_POS3_DIRECTION::DOWN) { + Log.warningln("3PosAnalog::mb_write - ID:%d Invalid remote value %d, clamping to MIDDLE(0)", id, networkValue); + clampedValue = E_POS3_DIRECTION::MIDDLE; + } + + if (clampedValue != remoteValue) + { + Log.verboseln("3PosAnalog::mb_write - ID:%d Remote value change %d -> %d", id, remoteValue, clampedValue); + remoteValue = clampedValue; + if (controlMode == POTControlMode::REMOTE) { + changed = true; + } + } + } + else if (addr == modbusAddress) + { + return MODBUS_ERROR_ILLEGAL_FUNCTION; + } + else + { + return E_INVALID_PARAMETER; + } + + if (changed && controlMode == POTControlMode::REMOTE) + { + if (value != remoteValue) + { + value = remoteValue; + notifyStateChange(); + } + } + + return E_OK; + } + + short mb_tcp_read(MB_Registers *reg) override + { + uint16_t addr = reg->startAddress; + if (addr == modbusAddress) { + return value; + } + else if (addr == modbusAddress + 1) { + return static_cast(controlMode); + } + else if (addr == modbusAddress + 2) { + return remoteValue; + } + return 0; + } + + void mb_tcp_register(ModbusTCP *manager) const override + { + ModbusBlockView *blocksView = mb_tcp_blocks(); + Component *thiz = const_cast(this); + for (int i = 0; i < blocksView->count; ++i) + { + MB_Registers info = blocksView->data[i]; + manager->registerModbus(thiz, info); + } + } + + ModbusBlockView *mb_tcp_blocks() const override + { + // Return the instance-specific Modbus block view + return &m_modbus_view; + } + + short serial_register(Bridge *bridge) override + { + bridge->registerMemberFunction(id, this, C_STR("info"), (ComponentFnPtr)&Pos3Analog::info); + return E_OK; + } + + int value; + const short upPin; + const short downPin; + +protected: + void notifyStateChange() override + { + Component::notifyStateChange(); + } + unsigned long last = 0; + +private: + int read() + { + bool up = RANGE(analogRead(upPin), ANALOG_INPUT_MIN_THRESHOLD_0, 1000); + bool down = RANGE(analogRead(downPin), ANALOG_INPUT_MIN_THRESHOLD_0, 1000); + int newDirection = E_POS3_DIRECTION::MIDDLE; + if (up && !down) + { + newDirection = E_POS3_DIRECTION::DOWN; + } + else if (down && !up) + { + newDirection = E_POS3_DIRECTION::UP; + } + else if (up && down) + { + newDirection = E_POS3_DIRECTION::INVALID; + } + return newDirection; + } +}; + +#endif diff --git a/src/components/AnalogLevelSwitch.cpp b/src/components/AnalogLevelSwitch.cpp new file mode 100644 index 00000000..11f9565f --- /dev/null +++ b/src/components/AnalogLevelSwitch.cpp @@ -0,0 +1,324 @@ +// ============================================================================ +// AnalogLevelSwitch – *revisited* +// -------------------------------------------------------------------------- +// Drop‑in replacement for the original AnalogLevelSwitch class. +// • Adds static hysteresis around level boundaries to prevent dithering. +// • Uses constexpr, PROGMEM strings and wider arithmetic to avoid overflow. +// • Allows optional exponential smoothing at compile‑time. +// • Keeps the public API identical, so existing sketches don't break. +// ---------------------------------------------------------------------------- +// © 2025 (MIT‑licensed) +// ============================================================================ + +#include "AnalogLevelSwitch.h" +#include +#include + +// ───────────────────────────────────────────────────────────────────────────── +// Compile‑time configuration +// ───────────────────────────────────────────────────────────────────────────── +#ifndef ANALOG_LVL_SLOTS_MAX +#define ANALOG_LVL_SLOTS_MAX 32 // upper bound enforced at runtime +#endif + +#ifndef ALS_SMOOTHING_SIZE +#define ALS_SMOOTHING_SIZE 8 // samples in moving‑average buffer +#endif + +#ifndef ALS_DEBOUNCE_COUNT +#define ALS_DEBOUNCE_COUNT 3 // identical detections before commit +#endif + +#ifndef ALS_HYSTERESIS_CODES +#define ALS_HYSTERESIS_CODES 4 // ±ADC codes guard‑band +#endif + +#ifndef ALS_READ_INTERVAL_MS +#define ALS_READ_INTERVAL_MS 25 +#endif + +#ifndef ALS_USE_EMA // undef to keep simple moving average +#define ALS_USE_EMA 0 // 0 = MA, 1 = EMA(α = 1/ALS_SMOOTHING_SIZE) +#endif + +// ───────────────────────────────────────────────────────────────────────────── +// Helpers / macro sugar +// ───────────────────────────────────────────────────────────────────────────── +#if defined(ARDUINO_ARCH_AVR) + #define ALS_L(FSTR) F(FSTR) +#else + #define ALS_L(FSTR) (FSTR) +#endif + +static constexpr uint16_t ADC_MAX_VALUE = 4095; // 12‑bit default + +// Forward declarations (private helpers) ………………………………………… +namespace { + template + static constexpr T clamp(T val, U lo, U hi) { + return val < lo ? lo : (val > hi ? hi : val); + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// CTOR +// ───────────────────────────────────────────────────────────────────────────── +AnalogLevelSwitch::AnalogLevelSwitch(Component *owner, + short _analogPin, + uint16_t _numLevels, + uint16_t _levelStep, + uint16_t _adcValueOffset, + short _id, + uint16_t _modbusAddress) + : Component("AnalogLevelSwitch", _id, Component::COMPONENT_DEFAULT, owner), + m_pin (_analogPin), + m_slotCount ((_numLevels > 0 && _numLevels <= ANALOG_LVL_SLOTS_MAX) ? _numLevels + : (_numLevels > ANALOG_LVL_SLOTS_MAX ? ANALOG_LVL_SLOTS_MAX : 1)), + m_adcStepPerSlot (_levelStep > 0 ? _levelStep : 1), + m_adcOffset (_adcValueOffset), + m_modbusAddr (_modbusAddress), + m_activeSlot (0), + m_adcRaw (0), + m_bufferIdx (0), + m_bufferSum (0), + m_adcSmoothed (0), + m_proposedSlot (0), + m_confirmCount (0), + m_modbusBlockCount (0) +{ + setNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS); + + // ── Sanity logging ────────────────────────────────────────────── + if (_numLevels <= 0) + Log.warningln(ALS_L("ALS ID:%d: numLevels invalid, using 1."), id); + else if (_numLevels > ANALOG_LVL_SLOTS_MAX) + Log.warningln(ALS_L("ALS ID:%d: numLevels %d > MAX %d, clamping to MAX."), + id, _numLevels, ANALOG_LVL_SLOTS_MAX); + + if (_levelStep <= 0) + Log.warningln(ALS_L("ALS ID:%d: levelStep invalid, using 1."), id); + + uint32_t maxCfgAdc = (uint32_t)m_adcOffset + (uint32_t)m_slotCount * (uint32_t)m_adcStepPerSlot - 1; + if (maxCfgAdc > ADC_MAX_VALUE) + Log.warningln(ALS_L("ALS ID:%d: Max slot ADC val (%lu) > ADC_MAX (%d)."), id, maxCfgAdc, ADC_MAX_VALUE); + + if (m_adcOffset >= ADC_MAX_VALUE) + Log.errorln(ALS_L("ALS ID:%d: adcOffset (%d) >= ADC_MAX (%d). Slots unreadable."), id, m_adcOffset, ADC_MAX_VALUE); + + // ── Initialise smoothing buffer ───────────────────────────────── + memset(m_adcBuffer, 0, sizeof(m_adcBuffer)); + uint16_t initialRaw = analogRead(m_pin); + for (uint8_t i = 0; i < ALS_SMOOTHING_SIZE; ++i) m_adcBuffer[i] = initialRaw; + m_bufferSum = (uint32_t)initialRaw * ALS_SMOOTHING_SIZE; + m_adcSmoothed = initialRaw; + m_adcRaw = initialRaw; + + m_activeSlot = determineSlotFromValue(m_adcSmoothed); + m_proposedSlot = m_activeSlot; + m_confirmCount = ALS_DEBOUNCE_COUNT; + + // ── Build static Modbus block table (unchanged) ───────────────── + buildModbusBlocks(); +} + +// ───────────────────────────────────────────────────────────────────────────── +// Private: buildModbusBlocks() +// ───────────────────────────────────────────────────────────────────────────── +void AnalogLevelSwitch::buildModbusBlocks() { + memset(m_modbusBlocks, 0, sizeof(m_modbusBlocks)); + const char *anaLvlGroup = "AnaLvl"; + const char *coilGroup = "Coil"; + + uint8_t idx = 0; + + // Detected level register + m_modbusBlocks[idx++] = { + static_cast(m_modbusAddr + static_cast(AnalogLevelRegOffset::DETECTED_LEVEL)), + 1, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, + static_cast(id), static_cast(AnalogLevelRegOffset::DETECTED_LEVEL), + "Detected Level", anaLvlGroup }; + + // Raw ADC value register + m_modbusBlocks[idx++] = { + static_cast(m_modbusAddr + static_cast(AnalogLevelRegOffset::RAW_ANALOG_VALUE)), + 1, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, + static_cast(id), static_cast(AnalogLevelRegOffset::RAW_ANALOG_VALUE), + "Raw Analog Value", anaLvlGroup }; + + // Per‑slot coil‑style bits + for (uint16_t s = 0; s < m_slotCount && idx < (2 + ANALOG_LVL_SLOTS_MAX); ++s) { + uint16_t regOff = static_cast(AnalogLevelRegOffset::LEVEL_STATE_START) + s; + m_modbusBlocks[idx++] = { + static_cast(m_modbusAddr + regOff), + 1, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, + static_cast(id), regOff, + "Level Slot Active", coilGroup }; + } + + m_modbusBlockCount = idx; + m_modbusView.data = m_modbusBlocks; + m_modbusView.count = m_modbusBlockCount; +} + +// ───────────────────────────────────────────────────────────────────────────── +// setup() +// ───────────────────────────────────────────────────────────────────────────── +short AnalogLevelSwitch::setup() { + Component::setup(); + +#if defined(ESP32) + analogReadResolution(12); + analogSetPinAttenuation(m_pin, ADC_11db); // 0‑3.6 V full‑scale +#endif + + pinMode(m_pin, INPUT); + + // Prime moving average / EMA buffer + uint16_t first = analogRead(m_pin); +#if ALS_USE_EMA + m_adcSmoothed = first; +#else + for (uint8_t i = 0; i < ALS_SMOOTHING_SIZE; ++i) m_adcBuffer[i] = first; + m_bufferSum = (uint32_t)first * ALS_SMOOTHING_SIZE; + m_adcSmoothed = first; +#endif + m_adcRaw = first; + m_activeSlot = determineSlotFromValue(m_adcSmoothed); + m_proposedSlot= m_activeSlot; + m_confirmCount= ALS_DEBOUNCE_COUNT; + + Log.verboseln(ALS_L("ALS ID:%d setup. Pin:%d, Slots:%d, Step:%d, Offset:%d, Smooth:%d, Debounce:%d, Hysteresis:%d, Initial Slot:%d, Raw:%d"), + id, m_pin, m_slotCount, m_adcStepPerSlot, m_adcOffset, + ALS_SMOOTHING_SIZE, ALS_DEBOUNCE_COUNT, ALS_HYSTERESIS_CODES, + m_activeSlot, m_adcRaw); + return E_OK; +} + +// ───────────────────────────────────────────────────────────────────────────── +// determineSlotFromValue() – with static hysteresis +// ───────────────────────────────────────────────────────────────────────────── +uint16_t AnalogLevelSwitch::determineSlotFromValue(uint16_t adcVal, uint16_t currentSlot /*= UINT16_MAX*/) const { + // If caller provides currentSlot we clip searches to ±1 neighbor for speed. + uint16_t slotLo = 0; + uint16_t slotHi = m_slotCount - 1; + if (currentSlot != UINT16_MAX) { + slotLo = (currentSlot > 0) ? currentSlot - 1 : 0; + slotHi = (currentSlot + 1 < m_slotCount) ? currentSlot + 1 : m_slotCount - 1; + } + + for (uint16_t s = slotLo; s <= slotHi; ++s) { + uint32_t lowThr = (uint32_t)m_adcOffset + (uint32_t)m_adcStepPerSlot * s - ALS_HYSTERESIS_CODES; + uint32_t highThr = lowThr + m_adcStepPerSlot + 2 * ALS_HYSTERESIS_CODES; + if (adcVal < lowThr) continue; + if (adcVal < highThr) return s; + } + // If we fall through, clamp to ends. + return (adcVal < (uint32_t)m_adcOffset) ? 0 : (m_slotCount - 1); +} + +// ───────────────────────────────────────────────────────────────────────────── +// loop() +// ───────────────────────────────────────────────────────────────────────────── +short AnalogLevelSwitch::loop() { + Component::loop(); + if (now - m_lastReadMs < ALS_READ_INTERVAL_MS) return E_OK; + m_lastReadMs = now; + + // ── Acquire & smooth sample ───────────────────────────────────── + m_adcRaw = analogRead(m_pin); + +#if ALS_USE_EMA + // α = 1/n (n = smoothing size) + m_adcSmoothed = m_adcSmoothed - (m_adcSmoothed / ALS_SMOOTHING_SIZE) + (m_adcRaw / ALS_SMOOTHING_SIZE); +#else + m_bufferSum -= m_adcBuffer[m_bufferIdx]; + m_adcBuffer[m_bufferIdx] = m_adcRaw; + m_bufferSum += m_adcRaw; + m_bufferIdx = (m_bufferIdx + 1) % ALS_SMOOTHING_SIZE; + m_adcSmoothed = m_bufferSum / ALS_SMOOTHING_SIZE; +#endif + + // ── Hysteresis + debounce ────────────────────────────────────── + uint16_t candidate = determineSlotFromValue(m_adcSmoothed, m_proposedSlot); + + if (candidate == m_proposedSlot) { + if (m_confirmCount < ALS_DEBOUNCE_COUNT) ++m_confirmCount; + } else { + m_proposedSlot = candidate; + m_confirmCount = 1; + } + + if (m_confirmCount >= ALS_DEBOUNCE_COUNT && m_proposedSlot != m_activeSlot) { + Log.verboseln(ALS_L("ALS ID:%d: Slot %d → %d (Raw:%d Smooth:%d)"), + id, m_activeSlot, m_proposedSlot, m_adcRaw, m_adcSmoothed); + m_activeSlot = m_proposedSlot; + notifyStateChange(); + } + return E_OK; +} + +// ───────────────────────────────────────────────────────────────────────────── +// info(), Modbus stubs, etc. (mostly unchanged, minor formatting tweaks) +// ───────────────────────────────────────────────────────────────────────────── +short AnalogLevelSwitch::info(short, short) { + Log.infoln(ALS_L("AnalogLevelSwitch::info – ID:%d"), id); + Log.infoln(ALS_L(" Pin:%d Slots:%d Step:%d Offset:%d"), m_pin, m_slotCount, m_adcStepPerSlot, m_adcOffset); + Log.infoln(ALS_L(" Smooth:%d Debounce:%d Hyst:%d ADCMax:%d"), + ALS_SMOOTHING_SIZE, ALS_DEBOUNCE_COUNT, ALS_HYSTERESIS_CODES, ADC_MAX_VALUE); + Log.infoln(ALS_L(" Current:%d Raw:%d Smoothed:%d"), m_activeSlot, m_adcRaw, m_adcSmoothed); + return E_OK; +} + +void AnalogLevelSwitch::notifyStateChange() { + Component::notifyStateChange(); +} + +// Modbus read/write/registration – unchanged from original version … +short AnalogLevelSwitch::mb_tcp_write(MB_Registers *reg, short netVal) { + uint16_t addr = reg->startAddress; + uint16_t base = m_modbusAddr + static_cast(AnalogLevelRegOffset::LEVEL_STATE_START); + uint16_t end = base + m_slotCount; + + if (addr >= base && addr < end) { + Log.warningln(ALS_L("ALS ID:%d: Write to slot‑state address %d not supported."), id, addr); + return MODBUS_ERROR_ILLEGAL_FUNCTION; + } + if (addr == m_modbusAddr + static_cast(AnalogLevelRegOffset::DETECTED_LEVEL) || + addr == m_modbusAddr + static_cast(AnalogLevelRegOffset::RAW_ANALOG_VALUE)) { + Log.warningln(ALS_L("ALS ID:%d: Write to read‑only address %d."), id, addr); + return MODBUS_ERROR_ILLEGAL_FUNCTION; + } + Log.warningln(ALS_L("ALS ID:%d: Write to unknown address %d."), id, addr); + return MODBUS_ERROR_ILLEGAL_DATA_ADDRESS; +} + +short AnalogLevelSwitch::mb_tcp_read(MB_Registers *reg) { + uint16_t addr = reg->startAddress; + if (addr == m_modbusAddr + static_cast(AnalogLevelRegOffset::DETECTED_LEVEL)) return m_activeSlot; + if (addr == m_modbusAddr + static_cast(AnalogLevelRegOffset::RAW_ANALOG_VALUE)) return m_adcRaw; + + uint16_t base = m_modbusAddr + static_cast(AnalogLevelRegOffset::LEVEL_STATE_START); + uint16_t end = base + m_slotCount; + if (addr >= base && addr < end) { + uint16_t slotIdx = addr - base; + return (slotIdx == m_activeSlot) ? 1 : 0; + } + Log.warningln(ALS_L("ALS ID:%d: Read from unknown address %d."), id, addr); + return 0; +} + +void AnalogLevelSwitch::mb_tcp_register(ModbusTCP *mgr) const { + auto *blocks = mb_tcp_blocks(); + if (!blocks || !blocks->data) return; + Component *thiz = const_cast(this); + for (uint8_t i = 0; i < blocks->count; ++i) mgr->registerModbus(thiz, blocks->data[i]); +} + +ModbusBlockView *AnalogLevelSwitch::mb_tcp_blocks() const { + return const_cast(&m_modbusView); +} + +short AnalogLevelSwitch::serial_register(Bridge *bridge) { + bridge->registerMemberFunction(id, this, C_STR("info"), (ComponentFnPtr)&AnalogLevelSwitch::info); + return E_OK; +} diff --git a/src/components/AnalogLevelSwitch.h b/src/components/AnalogLevelSwitch.h new file mode 100644 index 00000000..c935c986 --- /dev/null +++ b/src/components/AnalogLevelSwitch.h @@ -0,0 +1,224 @@ +/** + * @file AnalogLevelSwitch.h + * @brief Component to read an analog input as a multi-position switch. + * + * --- Resistor Selection for Voltage Divider Setup --- + * + * This component is designed to interpret an analog voltage as a discrete position or slot. + * It allows for an initial ADC offset (adcValueOffset), meaning the first slot does not + * necessarily start at an ADC reading of 0. + * + * Principle: + * A common way to achieve this is with a voltage divider. You'll have one analog input pin. + * The circuit typically involves one fixed resistor (R_fixed) and a set of switched resistors + * (R_sw0, R_sw1, ..., R_swN-1), one for each of the N slots. + * + * Example Case: + * - Number of slots (numLevels): 4 + * - ADC Value Offset (adcValueOffset): 200 (e.g., readings 0-199 are effectively below the first slot) + * - Slot Width (levelStep): 800 ADC counts per slot. + * - System Voltage (V_in): 5V + * - ADC Range: 0-4095 (0V -> 0, 5V -> 4095) + * + * Component Constructor Parameters: + * - numLevels: 4 + * - levelStep: 800 + * - adcValueOffset: 200 + * + * This means the ADC windows for slots are: + * - Slot 0: ADC readings from 200 to (200 + 800 - 1) = 999 + * - Slot 1: ADC readings from (200 + 800) = 1000 to (200 + 2*800 - 1) = 1799 + * - Slot 2: ADC readings from (200 + 2*800) = 1800 to (200 + 3*800 - 1) = 2599 + * - Slot 3: ADC readings from (200 + 3*800) = 2600 to (200 + 4*800 - 1) = 3399 + * The highest ADC value mapped to a slot is 3399. Readings above this (e.g. > 3399) + * will be clamped to the last slot (Slot 3). Readings below the offset (e.g. < 200) + * will be clamped to the first slot (Slot 0) by the component's logic. + * + * Circuit Configuration Example: + * - R_fixed is connected from the Analog Input Pin to Ground (GND). + * - For each slot, a different resistor (R_sw0, R_sw1, etc.) is connected from the + * Analog Input Pin to V_in (5V). + * - The voltage at the Analog Input Pin (V_out) is given by: + * V_out = V_in * (R_fixed / (R_sw_current + R_fixed)) (if R_sw to V_in, R_fixed to GND) + * Alternatively, if R_fixed to V_in and R_sw to GND (less common for increasing voltage with switch): + * V_out = V_in * (R_sw_current / (R_fixed + R_sw_current)) + * - Assuming the first configuration (R_fixed to GND, R_sw to V_in): + * The ADC reading is: ADC_value = (V_out / V_in) * 4095 (for this example's V_in and ADC range) + * + * Target ADC Values & Resistor Calculation (Adjusted for offset): + * Target midpoints for ADC windows: + * - Slot 0 (200-999): Midpoint ~ (200+999)/2 = 599 => V_out = (599/4095)*5V ~ 0.731V + * - Slot 1 (1000-1799): Midpoint ~ (1000+1799)/2 = 1399 => V_out = (1399/4095)*5V ~ 1.708V + * - Slot 2 (1800-2599): Midpoint ~ (1800+2599)/2 = 2199 => V_out = (2199/4095)*5V ~ 2.685V + * - Slot 3 (2600-3399): Midpoint ~ (2600+3399)/2 = 2999 => V_out = (2999/4095)*5V ~ 3.662V + * + * Let R_fixed = 10 kOhm (to GND). R_sw_current connects Analog Pin to 5V. + * V_out / V_in = R_fixed / (R_sw_current + R_fixed) => This formula gives decreasing V_out for increasing R_sw. + * For increasing V_out with slots, it's usually R_fixed to VCC and R_sw_current to GND for each slot, + * where V_out = VCC * (R_sw_current / (R_fixed + R_sw_current)). + * Or, a ladder network. The example below assumes R_fixed to GND, and R_sw to V_in, which creates HIGHER voltages for LOWER R_sw. + * This means R_sw needs to DECREASE to get higher voltages / higher slot numbers. + * V_out = V_in * R_fixed / (R_sw + R_fixed) is not what we want if R_sw is the switched part to V_in for *increasing* voltage steps. + * Let's re-evaluate the voltage divider formula application for this common use case: + * + * Corrected Circuit Configuration for Increasing Voltage with Slot Index: + * A simple way is multiple resistors (R0, R1, R2, R3) connected via a rotary switch to the analog pin. + * The other end of these resistors goes to V_in (5V). A single resistor R_pull_down goes from analog pin to GND. + * V_out = V_in * (R_pull_down / (R_current_switched_to_Vin + R_pull_down)). + * This means R_current_switched_to_Vin must DECREASE for V_out to INCREASE. + * Example Values (R_pull_down = 10k Ohm from Analog Pin to GND): + * + * - Slot 0 (V_out ~ 0.731V -> R_sw0 to 5V should be large): + * 0.731V = 5V * (10k / (R_sw0 + 10k)) => R_sw0 + 10k = 5V/0.731V * 10k = 68.4k => R_sw0 ~ 58.4 kOhm. (Std: 56k) + * Using 56k: V_out = 5V * (10k / (56k+10k)) ~ 0.757V; ADC ~ 620. (Slot 0: 200-999) + * + * - Slot 1 (V_out ~ 1.708V -> R_sw1 to 5V should be smaller): + * 1.708V = 5V * (10k / (R_sw1 + 10k)) => R_sw1 + 10k = 5V/1.708V * 10k = 29.27k => R_sw1 ~ 19.27 kOhm. (Std: 20k or 18k) + * Using 20k: V_out = 5V * (10k / (20k+10k)) ~ 1.667V; ADC ~ 1365. (Slot 1: 1000-1799) + * + * - Slot 2 (V_out ~ 2.685V -> R_sw2 to 5V should be smaller still): + * 2.685V = 5V * (10k / (R_sw2 + 10k)) => R_sw2 + 10k = 5V/2.685V * 10k = 18.62k => R_sw2 ~ 8.62 kOhm. (Std: 8.2k) + * Using 8.2k: V_out = 5V * (10k / (8.2k+10k)) ~ 2.747V; ADC ~ 2250. (Slot 2: 1800-2599) + * + * - Slot 3 (V_out ~ 3.662V -> R_sw3 to 5V should be smallest): + * 3.662V = 5V * (10k / (R_sw3 + 10k)) => R_sw3 + 10k = 5V/3.662V * 10k = 13.65k => R_sw3 ~ 3.65 kOhm. (Std: 3.6k) + * Using 3.6k: V_out = 5V * (10k / (3.6k+10k)) ~ 3.676V; ADC ~ 3011. (Slot 3: 2600-3399) + * + * Summary of example resistor values (R_pull_down = 10k to GND, V_in=5V, ADC Offset=200, Step=800): + * Each R_swX is connected between 5V and the Analog Input pin when its slot is active. + * - Slot 0: R_sw0 = 56k + * - Slot 1: R_sw1 = 20k + * - Slot 2: R_sw2 = 8.2k + * - Slot 3: R_sw3 = 3.6k + * + * Important Considerations: + * - Resistor Tolerances: Use 1% or better resistors if possible, or account for tolerances + * to ensure ADC readings for different slots don't overlap. + * - ADC Linearity & Noise: Real-world ADCs have non-linearities and noise. Provide sufficient + * margin between the target ADC values for each slot. + * - ADC Input Impedance: Ensure the equivalent resistance of the voltage divider is not too high, + * as it can affect ADC reading accuracy due to the ADC's input impedance and sample/hold capacitor charging. + * Typically, values in the 1k to 100k range for the overall divider are fine for many MCUs. + * - Debouncing: If the switch is mechanical, you might need software or hardware debouncing, + * though this component reads periodically based on ANALOG_SWITCH_READ_INTERVAL. + */ + +#ifndef ANALOG_LEVEL_SWITCH_H +#define ANALOG_LEVEL_SWITCH_H + +#include +#include "../config.h" // For ANALOG_SWITCH_READ_INTERVAL if not overridden by ALS_READ_INTERVAL_MS +#include +#include +#include "modbus/ModbusTCP.h" +#include "config-modbus.h" +#include // For uint16_t, uint32_t + +// ───────────────────────────────────────────────────────────────────────────── +// Compile‑time configuration (moved from .cpp) +// ───────────────────────────────────────────────────────────────────────────── +#ifndef ANALOG_LVL_SLOTS_MAX +#define ANALOG_LVL_SLOTS_MAX 32 // upper bound enforced at runtime +#endif + +#ifndef ALS_SMOOTHING_SIZE +#define ALS_SMOOTHING_SIZE 8 // samples in moving‑average buffer +#endif + +#ifndef ALS_DEBOUNCE_COUNT +#define ALS_DEBOUNCE_COUNT 3 // identical detections before commit +#endif + +#ifndef ALS_HYSTERESIS_CODES +#define ALS_HYSTERESIS_CODES 4 // ±ADC codes guard‑band +#endif + +#ifndef ALS_READ_INTERVAL_MS +#define ALS_READ_INTERVAL_MS 25 // Override for ANALOG_SWITCH_READ_INTERVAL +#endif + +#ifndef ALS_USE_EMA // undef to keep simple moving average +#define ALS_USE_EMA 0 // 0 = MA, 1 = EMA(α = 1/ALS_SMOOTHING_SIZE) +#endif +// ───────────────────────────────────────────────────────────────────────────── + +class Bridge; + +class AnalogLevelSwitch : public Component +{ +public: + // Removed old static const members, replaced by defines above + // static const short SMOOTHING_ARRAY_SIZE = 10; + // static const short MAX_ANALOG_LEVELS = 16; + // static const short DEBOUNCE_CONFIRMATIONS_COUNT = 3; + + enum class AnalogLevelRegOffset : uint16_t { // Changed underlying type to uint16_t + DETECTED_LEVEL = 0, + RAW_ANALOG_VALUE = 1, + LEVEL_STATE_START = 2 + }; + +private: + const short m_pin; + const uint16_t m_slotCount; + const uint16_t m_adcStepPerSlot; // Changed from int + const uint16_t m_adcOffset; // Changed from int + const uint16_t m_modbusAddr; + + uint16_t m_activeSlot; + uint16_t m_adcRaw; + + // Smoothing data (fixed array) + uint16_t m_adcBuffer[ALS_SMOOTHING_SIZE]; // Use new define + uint16_t m_bufferIdx = 0; + uint32_t m_bufferSum = 0; // Changed from long to uint32_t for consistency + uint16_t m_adcSmoothed = 0; + + // Debouncing data + uint16_t m_proposedSlot = 0; + uint16_t m_confirmCount = 0; + + // Modbus definitions + MB_Registers m_modbusBlocks[2 + ANALOG_LVL_SLOTS_MAX]; // Use new define + uint16_t m_modbusBlockCount = 0; + ModbusBlockView m_modbusView; + + // Private helpers + void buildModbusBlocks(); // Added declaration + // Updated signature for determineSlotFromValue + uint16_t determineSlotFromValue(uint16_t adcVal, uint16_t currentSlot = UINT16_MAX) const; + + +public: + AnalogLevelSwitch( + Component *owner, + short _analogPin, + uint16_t _numLevels, // Changed from ushort/short + uint16_t _levelStep, // Changed from int + uint16_t _adcValueOffset,// Changed from int + short _id, + uint16_t _modbusAddress); // Changed from ushort + + short setup() override; + short loop() override; + short info(short val0 = 0, short val1 = 0) override; + short debug() override { return info(0, 0); } + + // Return types adjusted + uint16_t getActiveSlot() const { return m_activeSlot; } // Changed from ushort + uint16_t getRawAdc() const { return m_adcRaw; } // Changed from ushort + uint16_t getSmoothedAdc() const { return m_adcSmoothed; } // Changed from ushort + + short mb_tcp_write(MB_Registers *reg, short networkValue) override; + short mb_tcp_read(MB_Registers *reg) override; + void mb_tcp_register(ModbusTCP *manager) const override; + ModbusBlockView *mb_tcp_blocks() const override; + + short serial_register(Bridge *bridge) override; + +protected: + void notifyStateChange() override; + unsigned long m_lastReadMs = 0; +}; + +#endif // ANALOG_LEVEL_SWITCH_H \ No newline at end of file diff --git a/src/components/Extruder.cpp b/src/components/Extruder.cpp new file mode 100644 index 00000000..b408c308 --- /dev/null +++ b/src/components/Extruder.cpp @@ -0,0 +1,517 @@ +#include "Extruder.h" +#include + +Extruder::Extruder(Component *owner, SAKO_VFD *vfd, Pos3Analog *joystick, POT *speedPot, POT *overloadPot) + : Component(EXTRUDER_COMPONENT_NAME, COMPONENT_KEY_EXTRUDER, Component::COMPONENT_DEFAULT, owner), + _vfd(vfd), + _joystick(joystick), + _speedPot(speedPot), + _overloadPot(overloadPot), + _currentState(ExtruderState::IDLE), + _lastJoystickDirection(Pos3Analog::E_POS3_DIRECTION::MIDDLE), + _currentSpeedPotValue(100), + _currentOverloadPotValue(100), + _calculatedExtrudingSpeedHz(EXTRUDER_SPEED_MEDIUM_HZ), + _calculatedOverloadThresholdPercent(EXTRUDER_OVERLOAD_POT_MAX_TORQUE_PERCENT), + _lastStateChangeTimeMs(0), + _jammedStartTimeMs(0), + _lastVfdReadTimeMs(0), + _joystickHoldStartTimeMs(0), + _modbusCommandRegisterValue(static_cast(ExtruderModbusCommand::NO_COMMAND)), + _operationStartTimeMs(0), + _currentMaxOperationTimeMs(0) +{ + if (!_vfd) + { + Log.errorln("[%s] ERROR: VFD pointer is null!", name.c_str()); + return; + } + setNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS); +} + +short Extruder::init() +{ + if (!_vfd) + { + Log.errorln("[%s] ERROR: Cannot initialize - VFD pointer is null!", name.c_str()); + return E_INVALID_PARAMETER; + } + Log.infoln("[%s] init() called. Setting to default state.", name.c_str()); + + _updatePotValues(); + _vfdStop(); + _transitionToState(ExtruderState::IDLE); + + _lastJoystickDirection = Pos3Analog::E_POS3_DIRECTION::MIDDLE; + _lastStateChangeTimeMs = millis(); + _jammedStartTimeMs = 0; + _lastVfdReadTimeMs = 0; + _joystickHoldStartTimeMs = 0; + _operationStartTimeMs = 0; + _currentMaxOperationTimeMs = 0; + _modbusCommandRegisterValue = static_cast(ExtruderModbusCommand::NO_COMMAND); + + return E_OK; +} + +short Extruder::setup() +{ + Component::setup(); + return this->init(); +} + +void Extruder::_transitionToState(ExtruderState newState) +{ + if (_currentState != newState) + { + Log.verboseln("[%s] State transition: %d -> %d", name.c_str(), static_cast(_currentState), static_cast(newState)); + _currentState = newState; + _lastStateChangeTimeMs = millis(); + if (newState != ExtruderState::JAMMED) + { + _jammedStartTimeMs = 0; + } + if (newState == ExtruderState::IDLE || + newState == ExtruderState::STOPPING || + newState == ExtruderState::JAMMED || + newState == ExtruderState::RESETTING_JAM || + newState == ExtruderState::EXTRUDING_AUTO) + { + _joystickHoldStartTimeMs = 0; + } + + if (newState == ExtruderState::EXTRUDING_MANUAL || newState == ExtruderState::EXTRUDING_AUTO) + { + _operationStartTimeMs = millis(); + float currentSpeedHzForCalc = _calculatedExtrudingSpeedHz; + + if (currentSpeedHzForCalc > 0 && EXTRUDER_SPEED_MEDIUM_HZ > 0) + { + _currentMaxOperationTimeMs = static_cast( + EXTRUDER_MAX_RUN_TIME_MEDIUM_SPEED_MS * + (static_cast(EXTRUDER_SPEED_MEDIUM_HZ) / currentSpeedHzForCalc)); + Log.verboseln("[%s] New operation. Max time: %lu ms for speed %.2f (0.01Hz)", name.c_str(), _currentMaxOperationTimeMs, currentSpeedHzForCalc); + } + else + { + _currentMaxOperationTimeMs = EXTRUDER_MAX_RUN_TIME_MEDIUM_SPEED_MS; + Log.warningln("[%s] Operation speed for max time calc is zero or invalid. Defaulting max time to %lu ms.", name.c_str(), _currentMaxOperationTimeMs); + } + } + else + { + _operationStartTimeMs = 0; + _currentMaxOperationTimeMs = 0; + } + } +} + +void Extruder::_updatePotValues() +{ + _currentSpeedPotValue = _speedPot ? _speedPot->getValue() : 100; + float multiplier = EXTRUDER_SPEED_POT_MIN_MULTIPLIER + + (EXTRUDER_SPEED_POT_MAX_MULTIPLIER - EXTRUDER_SPEED_POT_MIN_MULTIPLIER) * + (_currentSpeedPotValue / 100.0f); + _calculatedExtrudingSpeedHz = static_cast(EXTRUDER_SPEED_MEDIUM_HZ) * multiplier; + + _currentOverloadPotValue = _overloadPot ? _overloadPot->getValue() : 100; + _calculatedOverloadThresholdPercent = static_cast(map(_currentOverloadPotValue, 0, 100, EXTRUDER_OVERLOAD_POT_MIN_TORQUE_PERCENT, EXTRUDER_OVERLOAD_POT_MAX_TORQUE_PERCENT)); + _calculatedOverloadThresholdPercent = constrain(_calculatedOverloadThresholdPercent, EXTRUDER_OVERLOAD_POT_MIN_TORQUE_PERCENT, EXTRUDER_OVERLOAD_POT_MAX_TORQUE_PERCENT); +} + +void Extruder::_vfdStartForward(uint16_t frequencyCentiHz) +{ + if (!_vfd) + return; + Log.verboseln("[%s] VFD Start Forward: %d (0.01Hz units)", name.c_str(), frequencyCentiHz); + _vfd->setFrequency(frequencyCentiHz); + _vfd->run(); +} + +void Extruder::_vfdStartReverse(uint16_t frequencyCentiHz) +{ + if (!_vfd) + return; + Log.verboseln("[%s] VFD Start Reverse: %d (0.01Hz units)", name.c_str(), frequencyCentiHz); + _vfd->setFrequency(frequencyCentiHz); + _vfd->reverse(); +} + +void Extruder::_vfdStop() +{ + if (!_vfd) + return; + Log.verboseln("[%s] VFD Stop", name.c_str()); + _vfd->stop(); +} + +void Extruder::_vfdResetJam() +{ + if (!_vfd) + return; + Log.verboseln("[%s] VFD Resetting Fault/Jam", name.c_str()); + _vfd->resetFault(); +} + +void Extruder::_checkVfdForJam() +{ + if (!_vfd) + return; + if (!(_currentState == ExtruderState::EXTRUDING_MANUAL || _currentState == ExtruderState::EXTRUDING_AUTO)) + return; + + short currentTorque = _vfd->getTorque(); + + if (_vfd->hasFault()) + { + Log.errorln("[%s] JAMMED! VFD reports fault code %d.", name.c_str(), _vfd->getFaultCode()); + _transitionToState(ExtruderState::JAMMED); + return; + } + + if (currentTorque > _calculatedOverloadThresholdPercent) + { + if (_jammedStartTimeMs == 0) + { + _jammedStartTimeMs = millis(); + Log.warningln("[%s] High torque detected (%d%% > %d%%). Monitoring for jam.", name.c_str(), currentTorque, _calculatedOverloadThresholdPercent); + } + else if (millis() - _jammedStartTimeMs > 1000) + { + Log.errorln("[%s] JAMMED! Torque %d%% > %d%% for >%lums.", name.c_str(), currentTorque, _calculatedOverloadThresholdPercent, 1000); + _transitionToState(ExtruderState::JAMMED); + } + } + else + { + if (_jammedStartTimeMs != 0) + { + Log.infoln("[%s] Torque (%d%%) normalized below threshold (%d%%). Jam monitor reset.", name.c_str(), currentTorque, _calculatedOverloadThresholdPercent); + } + _jammedStartTimeMs = 0; + } +} + +short Extruder::loop() +{ + Component::loop(); + if (!_vfd) + { + return E_INVALID_PARAMETER; + } + return E_OK; + _updatePotValues(); + Pos3Analog::E_POS3_DIRECTION currentJoystickDir = _joystick ? static_cast(_joystick->getValue()) : Pos3Analog::E_POS3_DIRECTION::MIDDLE; + if ((_currentState == ExtruderState::EXTRUDING_MANUAL || _currentState == ExtruderState::EXTRUDING_AUTO) && + _operationStartTimeMs > 0 && _currentMaxOperationTimeMs > 0) + { + if (now - _operationStartTimeMs > _currentMaxOperationTimeMs) + { + Log.warningln("[%s] MAX OPERATION TIME (%lu ms) EXCEEDED for state %d! Stopping.", + name.c_str(), _currentMaxOperationTimeMs, static_cast(_currentState)); + _transitionToState(ExtruderState::STOPPING); + } + } + + if ((_currentState == ExtruderState::EXTRUDING_MANUAL || _currentState == ExtruderState::EXTRUDING_AUTO) && + (now - _lastVfdReadTimeMs > EXTRUDER_VFD_READ_INTERVAL_MS)) + { + _checkVfdForJam(); + _lastVfdReadTimeMs = now; + } + + switch (_currentState) + { + case ExtruderState::IDLE: + _handleIdleState(); + break; + case ExtruderState::EXTRUDING_MANUAL: + _handleExtrudingManualState(); + break; + case ExtruderState::EXTRUDING_AUTO: + _handleExtrudingAutoState(); + break; + case ExtruderState::STOPPING: + _handleStoppingState(); + break; + case ExtruderState::JAMMED: + _handleJammedState(); + break; + case ExtruderState::RESETTING_JAM: + _handleResettingJamState(); + break; + default: + Log.warningln("[%s] Unknown state: %d. Transitioning to IDLE.", name.c_str(), static_cast(_currentState)); + _transitionToState(ExtruderState::IDLE); + break; + } + _lastJoystickDirection = currentJoystickDir; + return E_OK; +} + +void Extruder::_handleIdleState() +{ + Pos3Analog::E_POS3_DIRECTION joyDir = _joystick ? static_cast(_joystick->getValue()) : Pos3Analog::E_POS3_DIRECTION::MIDDLE; + if (joyDir == Pos3Analog::E_POS3_DIRECTION::UP && _lastJoystickDirection != Pos3Analog::E_POS3_DIRECTION::UP) + { + Log.verboseln("[%s] Joystick UP from IDLE. Starting EXTRUDING_MANUAL.", name.c_str()); + _joystickHoldStartTimeMs = millis(); + _vfdStartForward(static_cast(_calculatedExtrudingSpeedHz)); + _transitionToState(ExtruderState::EXTRUDING_MANUAL); + } + if (_vfd->isRunning() && _joystickHoldStartTimeMs == 0) + { + Log.warningln("[%s] VFD running in IDLE state unexpectedly. Stopping.", name.c_str()); + _vfdStop(); + } +} + +void Extruder::_handleExtrudingManualState() +{ + Pos3Analog::E_POS3_DIRECTION joyDir = _joystick ? static_cast(_joystick->getValue()) : Pos3Analog::E_POS3_DIRECTION::MIDDLE; + + if (joyDir == Pos3Analog::E_POS3_DIRECTION::UP) + { + if (_joystickHoldStartTimeMs > 0 && (millis() - _joystickHoldStartTimeMs > EXTRUDER_AUTO_MODE_HOLD_DURATION_MS)) + { + Log.infoln("[%s] Joystick held UP for auto mode. Transitioning to EXTRUDING_AUTO.", name.c_str()); + _transitionToState(ExtruderState::EXTRUDING_AUTO); + } + } + else + { + Log.infoln("[%s] Joystick released/moved from UP during EXTRUDING_MANUAL. Stopping.", name.c_str()); + _transitionToState(ExtruderState::STOPPING); + } +} + +void Extruder::_handleExtrudingAutoState() +{ + Pos3Analog::E_POS3_DIRECTION joyDir = _joystick ? static_cast(_joystick->getValue()) : Pos3Analog::E_POS3_DIRECTION::MIDDLE; + + if (joyDir != Pos3Analog::E_POS3_DIRECTION::UP) + { + Log.infoln("[%s] Joystick moved from UP during EXTRUDING_AUTO. Aborting to STOPPING.", name.c_str()); + _transitionToState(ExtruderState::STOPPING); + } +} + +void Extruder::_handleStoppingState() +{ + _vfdStop(); + _joystickHoldStartTimeMs = 0; + _transitionToState(ExtruderState::IDLE); +} + +void Extruder::_handleJammedState() +{ + _vfdStop(); + _joystickHoldStartTimeMs = 0; + Pos3Analog::E_POS3_DIRECTION joyDir = _joystick ? static_cast(_joystick->getValue()) : Pos3Analog::E_POS3_DIRECTION::MIDDLE; + if (joyDir == Pos3Analog::E_POS3_DIRECTION::MIDDLE && _lastJoystickDirection != Pos3Analog::E_POS3_DIRECTION::MIDDLE) + { + Log.infoln("[%s] Jammed: Joystick moved to MIDDLE. Ready for reset attempt.", name.c_str()); + _transitionToState(ExtruderState::RESETTING_JAM); + } +} + +void Extruder::_handleResettingJamState() +{ + Pos3Analog::E_POS3_DIRECTION joyDir = _joystick ? static_cast(_joystick->getValue()) : Pos3Analog::E_POS3_DIRECTION::MIDDLE; + if (joyDir == Pos3Analog::E_POS3_DIRECTION::MIDDLE) + { + } + else if (joyDir != Pos3Analog::E_POS3_DIRECTION::MIDDLE && _lastJoystickDirection == Pos3Analog::E_POS3_DIRECTION::MIDDLE) + { + Log.infoln("[%s] Resetting Jam: Joystick moved from MIDDLE. Returning to IDLE. Manual VFD reset might be needed.", name.c_str()); + _vfdResetJam(); + _transitionToState(ExtruderState::IDLE); + } +} + +short Extruder::info() +{ + Log.verboseln("--- Extruder Info (ID: %d, Name: %s) ---", id, name.c_str()); + Log.verboseln("State: %d, LastJoy: %d, CurrentJoy: %d", + static_cast(_currentState), + static_cast(_lastJoystickDirection), + static_cast(_joystick->getValue())); + Log.verboseln("SpeedPOT: %d (-> %.2f 0.01Hz), OverloadPOT: %d (-> %d%% Torque)", + _currentSpeedPotValue, _calculatedExtrudingSpeedHz, + _currentOverloadPotValue, _calculatedOverloadThresholdPercent); + uint16_t freq = 0; + bool freqValid = _vfd->getFrequency(freq); + Log.verboseln("VFD: Running=%s, Fault=%s, FreqSet=%.2fHz, Torque=%d%%", + _vfd->isRunning() ? "YES" : "NO", + _vfd->hasFault() ? "YES" : "NO", + freqValid ? (freq / 100.0f) : -1.0f, + _vfd->getTorque()); + return E_OK; +} + +short Extruder::debug() +{ + return info(); +} + +short Extruder::serial_register(Bridge *b) +{ + if (!b) + return E_INVALID_PARAMETER; + b->registerMemberFunction(id, this, C_STR("info"), (ComponentFnPtr)&Extruder::info); + b->registerMemberFunction(id, this, C_STR("extrude"), (ComponentFnPtr)&Extruder::cmd_extrude); + b->registerMemberFunction(id, this, C_STR("stop"), (ComponentFnPtr)&Extruder::cmd_stop); + return E_OK; +} + +short Extruder::cmd_extrude() +{ + Log.infoln("[%s] cmd_extrude received.", name.c_str()); + if (_currentState == ExtruderState::IDLE) + { + Log.infoln("[%s] Initiating EXTRUDING_AUTO from command.", name.c_str()); + _vfdStartForward(static_cast(_calculatedExtrudingSpeedHz)); + _transitionToState(ExtruderState::EXTRUDING_AUTO); + return E_OK; + } + else + { + Log.warningln("[%s] cmd_extrude ignored. Current state is %d (not IDLE).", name.c_str(), static_cast(_currentState)); + return 1; + } +} + +short Extruder::cmd_stop() +{ + Log.infoln("[%s] cmd_stop received.", name.c_str()); + if (_currentState != ExtruderState::IDLE && _currentState != ExtruderState::STOPPING) + { + Log.infoln("[%s] Initiating STOPPING from command. Current state: %d", name.c_str(), static_cast(_currentState)); + _transitionToState(ExtruderState::STOPPING); + return E_OK; + } + else + { + Log.infoln("[%s] cmd_stop: Already IDLE or STOPPING. No action taken.", name.c_str()); + return E_OK; + } +} + +ModbusBlockView *Extruder::mb_tcp_blocks() const +{ + static MB_Registers blocks[EXTRUDER_MB_BLOCK_COUNT] = { + {static_cast(EXTRUDER_MB_BASE_ADDRESS + EXTRUDER_MB_STATE_OFFSET), + 1, + E_FN_CODE::FN_READ_HOLD_REGISTER, + MB_ACCESS_READ_ONLY, + static_cast(id), + EXTRUDER_MB_STATE_OFFSET, + "Extruder State", + EXTRUDER_COMPONENT_NAME}, + {static_cast(EXTRUDER_MB_BASE_ADDRESS + EXTRUDER_MB_COMMAND_OFFSET), + 1, + E_FN_CODE::FN_READ_HOLD_REGISTER, + MB_ACCESS_READ_WRITE, + static_cast(id), + EXTRUDER_MB_COMMAND_OFFSET, + "Extruder Command (0:None,2:Extrude,3:Stop,4:Info)", + EXTRUDER_COMPONENT_NAME}}; + static ModbusBlockView blockView = {blocks, EXTRUDER_MB_BLOCK_COUNT}; + return &blockView; +} + +void Extruder::mb_tcp_register(ModbusTCP *mgr) const +{ + if (!mgr) + return; + ModbusBlockView *blocksView = mb_tcp_blocks(); + Component *thiz = const_cast(this); + for (int i = 0; i < blocksView->count; ++i) + { + mgr->registerModbus(thiz, blocksView->data[i]); + } +} + +short Extruder::mb_tcp_read(MB_Registers *reg) +{ + if (!reg) + return MODBUS_ERROR_ILLEGAL_DATA_ADDRESS; + + uint16_t address = reg->startAddress; + + if (address == (EXTRUDER_MB_BASE_ADDRESS + EXTRUDER_MB_STATE_OFFSET)) + { + return static_cast(_currentState); + } + else if (address == (EXTRUDER_MB_BASE_ADDRESS + EXTRUDER_MB_COMMAND_OFFSET)) + { + return _modbusCommandRegisterValue; + } + return MODBUS_ERROR_ILLEGAL_DATA_ADDRESS; +} + +short Extruder::mb_tcp_write(MB_Registers *reg, short networkValue) +{ + if (!reg) + return MODBUS_ERROR_ILLEGAL_DATA_ADDRESS; + + uint16_t address = reg->startAddress; + + if (address == (EXTRUDER_MB_BASE_ADDRESS + EXTRUDER_MB_COMMAND_OFFSET)) + { + ExtruderModbusCommand cmd = static_cast(networkValue); + Log.infoln("[%s] Modbus Command Register write. Address: %d, Value: %d (Cmd: %d)", + name.c_str(), address, networkValue, static_cast(cmd)); + + _modbusCommandRegisterValue = networkValue; + + short result = E_OK; + switch (cmd) + { + case ExtruderModbusCommand::CMD_EXTRUDE: + result = this->cmd_extrude(); + break; + case ExtruderModbusCommand::CMD_STOP: + result = this->cmd_stop(); + break; + case ExtruderModbusCommand::CMD_INFO: + result = this->info(); + break; + case ExtruderModbusCommand::NO_COMMAND: + Log.verboseln("[%s] Modbus NO_COMMAND received.", name.c_str()); + break; + default: + Log.warningln("[%s] Unknown Modbus command received: %d", name.c_str(), networkValue); + result = MODBUS_ERROR_ILLEGAL_DATA_VALUE; + break; + } + + if (cmd != ExtruderModbusCommand::NO_COMMAND && result == E_OK) + { + _modbusCommandRegisterValue = static_cast(ExtruderModbusCommand::NO_COMMAND); + } + else if (result != E_OK && result != MODBUS_ERROR_ILLEGAL_DATA_VALUE) + { + _modbusCommandRegisterValue = static_cast(ExtruderModbusCommand::NO_COMMAND); + } + + return (result == E_OK || result == 1) ? E_OK : result; + } + else if (address == (EXTRUDER_MB_BASE_ADDRESS + EXTRUDER_MB_STATE_OFFSET)) + { + Log.warningln("[%s] mb_tcp_write: Attempt to write to read-only State register %d", name.c_str(), address); + return MODBUS_ERROR_ILLEGAL_FUNCTION; + } + + return MODBUS_ERROR_ILLEGAL_DATA_ADDRESS; +} + +short Extruder::reset() +{ + Log.infoln("[%s] reset() called. Stopping VFD, clearing faults, and re-initializing.", name.c_str()); + _vfdStop(); + _vfdResetJam(); + return this->init(); +} + +// ... rest of the file remains unchanged ... \ No newline at end of file diff --git a/src/components/Extruder.h b/src/components/Extruder.h new file mode 100644 index 00000000..b690dfe9 --- /dev/null +++ b/src/components/Extruder.h @@ -0,0 +1,124 @@ +#ifndef PLUNGER_H +#define PLUNGER_H + +#include "config.h" +#include +#include +#include "./SAKO_VFD.h" +#include "./3PosAnalog.h" +#include "./POT.h" +#include "enums.h" +#include "modbus/ModbusTCP.h" + +// Component Constants +#define EXTRUDER_COMPONENT_ID 760 +#define EXTRUDER_COMPONENT_NAME "Extruder" + +// Speed Presets (in 0.01 Hz units for SAKO_VFD) +#define EXTRUDER_SPEED_SLOW_HZ 10 // 10.00 Hz +#define EXTRUDER_SPEED_MEDIUM_HZ 25 // 25.00 Hz +#define EXTRUDER_SPEED_FAST_HZ 50 // 50.00 Hz (currently unused, but defined) + +// Speed POT Configuration for Extruding (multiplier for MEDIUM speed) +// POT value 0-100. Example: 0 maps to 0.5x, 50 maps to 1.0x, 100 maps to 1.5x MEDIUM speed. +#define EXTRUDER_SPEED_POT_MIN_MULTIPLIER 0.5f +#define EXTRUDER_SPEED_POT_MAX_MULTIPLIER 1.5f + +// Overload POT Configuration and Jamming (using VFD Torque %) +// POT value 0-100. Maps to a torque threshold in percent (0-100). +// SAKO_VFD component now provides getTorque() returning 0-100. +#define EXTRUDER_OVERLOAD_POT_MIN_TORQUE_PERCENT 50 // Example: 50% Torque +#define EXTRUDER_OVERLOAD_POT_MAX_TORQUE_PERCENT 95 // Example: 95% Torque +#define PLUNGER_JAMMED_DURATION_MS 2000 // Time torque must be above threshold to be JAMMED +#define EXTRUDER_VFD_READ_INTERVAL_MS 200 // How often to check VFD torque for jamming +#define EXTRUDER_AUTO_MODE_HOLD_DURATION_MS 2000 // Time joystick must be held for auto mode + +// Modbus Configuration +#define EXTRUDER_MB_BASE_ADDRESS EXTRUDER_COMPONENT_ID // Using component ID as base +#define EXTRUDER_MB_STATE_OFFSET 0 +#define EXTRUDER_MB_COMMAND_OFFSET 1 +#define EXTRUDER_MB_BLOCK_COUNT 2 +#define EXTRUDER_MAX_RUN_TIME_MEDIUM_SPEED_MS 15000 // Max runtime at medium speed + +enum class ExtruderModbusCommand : short { + NO_COMMAND = 0, + CMD_EXTRUDE = 2, + CMD_STOP = 3, + CMD_INFO = 4 +}; + +// Extruder States +enum class ExtruderState : uint8_t { + IDLE, + EXTRUDING_MANUAL, // Joystick held UP, VFD forwarding, monitoring for auto-mode hold time + EXTRUDING_AUTO, // Auto-extruding after joystick hold, VFD forwarding + STOPPING, // Transition state to stop VFD + JAMMED, + RESETTING_JAM // State to handle reset after jam +}; + +class Extruder : public Component { +public: + Extruder(Component* owner, SAKO_VFD* vfd, Pos3Analog* joystick = nullptr, POT* speedPot = nullptr, POT* overloadPot = nullptr); + ~Extruder() override = default; + + short setup() override; + short loop() override; + short info() override; + short debug() override; + short serial_register(Bridge* b) override; + short init(); + short reset(); + + // Modbus TCP Interface + ModbusBlockView* mb_tcp_blocks() const override; + void mb_tcp_register(ModbusTCP* mgr) const override; + short mb_tcp_read(MB_Registers* reg) override; + short mb_tcp_write(MB_Registers* reg, short networkValue) override; + + // Public commands for serial/external control + short cmd_extrude(); + short cmd_stop(); + +private: + SAKO_VFD* _vfd; + Pos3Analog* _joystick; + POT* _speedPot; + POT* _overloadPot; + + ExtruderState _currentState; + Pos3Analog::E_POS3_DIRECTION _lastJoystickDirection; + + uint16_t _currentSpeedPotValue; // 0-100, defaults to 100 if pot is null + uint16_t _currentOverloadPotValue; // 0-100, defaults to 100 if pot is null + float _calculatedExtrudingSpeedHz; // Calculated speed for extruding (0.01Hz units) + uint8_t _calculatedOverloadThresholdPercent; // Calculated torque threshold (0-100%) + + unsigned long _lastStateChangeTimeMs; + unsigned long _jammedStartTimeMs; + unsigned long _lastVfdReadTimeMs; + unsigned long _joystickHoldStartTimeMs; // Timer for joystick hold duration + short _modbusCommandRegisterValue; // Holds the current value of the Modbus command register + unsigned long _operationStartTimeMs; // Start time of current extrude operation + unsigned long _currentMaxOperationTimeMs; // Calculated max duration for current operation + + // Helper methods + void _handleIdleState(); + void _handleExtrudingManualState(); + void _handleExtrudingAutoState(); + void _handleStoppingState(); + void _handleJammedState(); + void _handleResettingJamState(); + + void _updatePotValues(); + void _checkVfdForJam(); + void _transitionToState(ExtruderState newState); + + // VFD interaction wrappers + void _vfdStartForward(uint16_t frequencyCentiHz); + void _vfdStartReverse(uint16_t frequencyCentiHz); + void _vfdStop(); + void _vfdResetJam(); +}; + +#endif // PLUNGER_H \ No newline at end of file diff --git a/src/components/GPIO.h b/src/components/GPIO.h new file mode 100644 index 00000000..0e3cb7f5 --- /dev/null +++ b/src/components/GPIO.h @@ -0,0 +1,572 @@ +#ifndef GPIO_H +#define GPIO_H +// #include +#include +#include +#include + +#include +#include +#include +#include +#include "modbus/ModbusTypes.h" +#include "modbus/ModbusTCP.h" +#include +// ----------------------------------------------------------------------------- +// GPIO pin number enumeration (unchanged) +// ----------------------------------------------------------------------------- +enum E_GPIO_Pin +{ + E_GPIO_0 = 0, + E_GPIO_1 = 1, + E_GPIO_2 = 2, + E_GPIO_3 = 3, + E_GPIO_4 = 4, + E_GPIO_5 = 5, + E_GPIO_6 = 6, + E_GPIO_7 = 7, + E_GPIO_8 = 8, + E_GPIO_9 = 9, + E_GPIO_10 = 10, + E_GPIO_11 = 11, + E_GPIO_12 = 12, + E_GPIO_13 = 13, + E_GPIO_14 = 14, + E_GPIO_15 = 15, + E_GPIO_16 = 16, + E_GPIO_17 = 17, + E_GPIO_18 = 18, + E_GPIO_19 = 19, + E_GPIO_20 = 20, + E_GPIO_21 = 21, + // Skipping 22‑34 – not available + E_GPIO_35 = 35, + E_GPIO_36 = 36, + E_GPIO_37 = 37, + E_GPIO_38 = 38, + E_GPIO_39 = 39, + E_GPIO_40 = 40, + E_GPIO_41 = 41, + E_GPIO_42 = 42, + E_GPIO_43 = 43, + E_GPIO_44 = 44, + E_GPIO_45 = 45, + E_GPIO_46 = 46, + E_GPIO_47 = 47, + E_GPIO_48 = 48 +}; + +// ----------------------------------------------------------------------------- +// MB_GPIO pin types / modes (unchanged) +// ----------------------------------------------------------------------------- +enum E_GPIO_Type +{ + E_GPIO_TYPE_UNKNOWN, + E_GPIO_TYPE_INPUT, + E_GPIO_TYPE_OUTPUT, + E_GPIO_TYPE_INPUT_PULLUP, + E_GPIO_TYPE_INPUT_PULLDOWN, + E_GPIO_TYPE_OUTPUT_OPEN_DRAIN, + E_GPIO_TYPE_ANALOG_INPUT, // ADC + E_GPIO_TYPE_TOUCH // capacitive touch +}; + +// ----------------------------------------------------------------------------- +// Configuration for a single managed MB_GPIO pin – inherits Modbus meta‑data and +// adds timing & caching so we can throttle operations and skip duplicates. +// ----------------------------------------------------------------------------- +struct GPIO_PinConfig +{ + // MB_Registers fields copied here + ushort startAddress = 0xFFFF; // Use 0xFFFF to indicate invalid/not set + ushort count = 1; // Assume 1 register per pin + ushort slaveId = 0; // Usually 0 for TCP/IP direct + E_FN_CODE type = E_FN_CODE::FN_NONE; + E_ModbusAccess access = MB_ACCESS_NONE; + ushort componentId = 0; // Will be overridden by owner + const char *name = nullptr; + const char *group = nullptr; + ComponentFnPtr writeCallbackFn = nullptr; + + // GPIO specific fields + E_GPIO_Pin pinNumber = E_GPIO_0; + E_GPIO_Type pinType = E_GPIO_TYPE_UNKNOWN; + + // Runtime helpers + uint32_t opIntervalMs = 100; + int cachedValue = INT_MIN; + unsigned long lastOpTimestamp = 0; + + // Constructor initializes all members + GPIO_PinConfig(E_GPIO_Pin pNum = E_GPIO_0, + E_GPIO_Type pType = E_GPIO_TYPE_UNKNOWN, + ushort addr = 0xFFFF, + E_FN_CODE fn = E_FN_CODE::FN_NONE, + E_ModbusAccess acc = MB_ACCESS_NONE, + uint32_t intervalMs = 100, + const char *regName = nullptr, + const char *regGroup = nullptr, + ComponentFnPtr cb = nullptr) + : startAddress(addr), + count(1), + slaveId(0), + type(fn), + access(acc), + // componentId(0), // Let owner set it + name(regName), + group(regGroup), + writeCallbackFn(cb), + pinNumber(pNum), + pinType(pType), + opIntervalMs(intervalMs), + cachedValue(INT_MIN), + lastOpTimestamp(0) // Default initialized + { + } +}; + +// ----------------------------------------------------------------------------- +// Capability bit‑mask (unchanged – table at end) +// ----------------------------------------------------------------------------- +// Define capabilities using a bitmask +enum E_GPIO_Capability : uint32_t +{ + CAP_NONE = 0, + CAP_RTC = 1 << 0, + CAP_ADC1 = 1 << 1, + CAP_ADC2 = 1 << 2, + CAP_TOUCH = 1 << 3, + CAP_JTAG_TCK = 1 << 4, // GPIO4 (TCK), GPIO39 (MTCK) + CAP_JTAG_TDI = 1 << 5, // GPIO3 (TDI?), GPIO41 (MTDI) + CAP_JTAG_TDO = 1 << 6, // GPIO3 (TDO?), GPIO40 (MTDO) + CAP_JTAG_TMS = 1 << 7, // GPIO42 (MTMS) + CAP_FSPI_CS0 = 1 << 8, + CAP_FSPI_CLK = 1 << 9, + CAP_FSPI_MISO = 1 << 10, // Q/IO1 + CAP_FSPI_MOSI = 1 << 11, // D/IO0 + CAP_FSPI_HD = 1 << 12, // IO3 / GPIO9 + CAP_FSPI_WP = 1 << 13, // IO2 / GPIO10, GPIO38 + CAP_FSPI_IO4 = 1 << 14, // GPIO14 + CAP_FSPI_IO5 = 1 << 15, // GPIO13 + CAP_FSPI_IO6 = 1 << 16, // GPIO11, GPIO37 + CAP_FSPI_IO7 = 1 << 17, // GPIO12, GPIO36 + CAP_UART0_TX = 1 << 18, // GPIO43 + CAP_UART0_RX = 1 << 19, // GPIO44 + CAP_UART1_TX = 1 << 20, // GPIO18 + CAP_UART1_RX = 1 << 21, // GPIO17 + CAP_UART2_TX = 1 << 22, // GPIO20 + CAP_UART2_RX = 1 << 23, // GPIO19 + CAP_I2C0_SDA = 1 << 24, // GPIO8 + CAP_I2C0_SCL = 1 << 25, // GPIO9 + CAP_CLK_OUT1 = 1 << 26, // GPIO2, GPIO20, GPIO41 + CAP_CLK_OUT2 = 1 << 27, // GPIO1, GPIO19, GPIO40 + CAP_CLK_OUT3 = 1 << 28, // GPIO0, GPIO39 + CAP_USB_D_PLUS = 1 << 29, // GPIO20 + CAP_USB_D_MINUS = 1 << 30, // GPIO19 + CAP_LED_BUILTIN = 1U << 31 // GPIO38 + // Note: RGB LED (GPIO48), XTAL, Strapping pins are not included in this bitmask +}; +// ----------------------------------------------------------------------------- +// MB_GPIO component – manages a *group* of pins +// ----------------------------------------------------------------------------- +class MB_GPIO : public Component +{ +private: + // Internal copy of the pin configurations + std::vector pinConfigs; + + // Internal helper to find config by Modbus address + const GPIO_PinConfig *findConfigByModbusAddress(ushort address) const + { + // Iterate internal vector + for (const auto &cfg : pinConfigs) // Use direct member access + { + if (cfg.startAddress == address) + return &cfg; + } + return nullptr; + } + + // Internal helper to find *non-const* config (needed for write) + GPIO_PinConfig *findConfigByModbusAddress(ushort address) + { + // Needs to iterate over the internal vector non-constantly + for (auto &cfg : pinConfigs) // Use direct member access + { + if (cfg.startAddress == address) + return &cfg; + } + return nullptr; + } + + // ------------------------------------------------------------------------- + // Raw read helper + // ------------------------------------------------------------------------- + int readPinInternal(E_GPIO_Pin pin, E_GPIO_Type type) const + { + return (type == E_GPIO_TYPE_ANALOG_INPUT) ? analogRead(static_cast(pin)) + : digitalRead(static_cast(pin)); + } + + // ------------------------------------------------------------------------- + // Raw *and throttled* write helper – skips duplicates & enforces interval. + // Returns E_OK even when operation skipped (so Modbus sees no error). + // ------------------------------------------------------------------------- + short writePinInternal(GPIO_PinConfig &cfg, int value) + { + if (!(cfg.pinType == E_GPIO_TYPE_OUTPUT || cfg.pinType == E_GPIO_TYPE_OUTPUT_OPEN_DRAIN)) + { + Log.warningln("MB_GPIO::writePinInternal - Pin %d not output type (%d)", (int)cfg.pinNumber, (int)cfg.pinType); + return E_INVALID_PARAMETER; + } + + unsigned long now = millis(); + if (value == cfg.cachedValue) + { + // duplicate value → skip silently + return E_OK; + } + if (now - cfg.lastOpTimestamp < cfg.opIntervalMs) + { + // too soon → skip + return E_OK; + } + + digitalWrite(static_cast(cfg.pinNumber), value ? HIGH : LOW); + cfg.cachedValue = value; + cfg.lastOpTimestamp = now; + return E_OK; + } + + // ------------------------------------------------------------------------- + // Modbus block storage + // ------------------------------------------------------------------------- + MB_Registers *mbRegisterBlocksData = nullptr; + int mbRegisterBlocksCount = 0; + ModbusBlockView mbBlockView; // {data,count} + +public: + // static const std::map pinCapabilities; + + // Constructor takes std::vector reference and copies the data + MB_GPIO(Component *owner, + short _id, + const std::vector &configs) // Accept const reference + : Component("GPIO_Group", _id, Component::COMPONENT_DEFAULT, owner), + pinConfigs(configs) // Initialize by copying + { + setNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS); + allocateAndPopulateMbBlocks(); // Uses the internal pinConfigs copy + Log.verboseln("MB_GPIO %d constructed - Copied %d configs, %d Modbus blocks prepared", id, (int)pinConfigs.size(), mbRegisterBlocksCount); + } + ~MB_GPIO() override { delete[] mbRegisterBlocksData; } // Destructor unchanged + + // Component API ------------------------------------------------------------ + short setup() override; + short loop() override; + short info(short flags = 0, short val = 0) override; + short debug() override { return info(); } + short serial_register(Bridge *bridge) override; + short load(const JsonObject &config); // Removed override, base Component doesn't have virtual load + + // Modbus TCP --------------------------------------------------------------- + short mb_tcp_read(short address) override; + short mb_tcp_read(MB_Registers *reg) override { return reg ? mb_tcp_read(reg->startAddress) : 0; } + short mb_tcp_write(short address, short networkValue) override; + short mb_tcp_write(MB_Registers *reg, short networkValue) override { return reg ? mb_tcp_write(reg->startAddress, networkValue) : E_INVALID_PARAMETER; } + ModbusBlockView *mb_tcp_blocks() const override { return const_cast(&mbBlockView); } + void mb_tcp_register(ModbusTCP *manager) const override; + +private: + void populateMbRegisterBlocks(); // Removed component_id parameter + void allocateAndPopulateMbBlocks(); // New helper combining allocation & population + short configurePinModeInternal(E_GPIO_Pin pin, E_GPIO_Type type); +}; + +// ----------------------------------------------------------------------------- +// Helper to manage Modbus block allocation and population +// ----------------------------------------------------------------------------- +inline void MB_GPIO::allocateAndPopulateMbBlocks() +{ + // Clear previous allocation if any + delete[] mbRegisterBlocksData; + mbRegisterBlocksData = nullptr; + mbRegisterBlocksCount = 0; + mbBlockView = {nullptr, 0}; + + if (pinConfigs.empty()) { + clearNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS); + return; + } + + setNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS); + + int mappedCount = 0; + for (const auto &cfg : pinConfigs) + { + if (cfg.startAddress != 0xFFFF) + ++mappedCount; + } + if (mappedCount > 0) + { + mbRegisterBlocksCount = mappedCount; + mbRegisterBlocksData = new MB_Registers[mappedCount]; + if (!mbRegisterBlocksData) + { + Log.fatalln("MB_GPIO %d: Failed to allocate Modbus blocks", id); + mbRegisterBlocksCount = 0; + clearNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS); + } + else + { + populateMbRegisterBlocks(); + mbBlockView = {mbRegisterBlocksData, mbRegisterBlocksCount}; + } + } else { + clearNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS); + } +} + +// ----------------------------------------------------------------------------- +// populateMbRegisterBlocks – uses internal pinConfigs Vector +// ----------------------------------------------------------------------------- +inline void MB_GPIO::populateMbRegisterBlocks() +{ + if (!mbRegisterBlocksData || mbRegisterBlocksCount == 0) + return; + int idx = 0; + for (const auto &cfg : pinConfigs) // Use direct member access + { + // Log.infoln("MB_GPIO %d: Populating block %d, Address %u, Count %u, Type %u, Access %u", id, idx, cfg.startAddress, cfg.count, cfg.type, cfg.access); + if (cfg.startAddress != 0xFFFF && idx < mbRegisterBlocksCount) + { + mbRegisterBlocksData[idx] = MB_Registers( + cfg.startAddress, cfg.count, cfg.type, cfg.access, + id, // Set the owner MB_GPIO component's ID + cfg.slaveId, cfg.name, cfg.group, cfg.writeCallbackFn + // No associatedPin in MB_Registers constructor + ); + ++idx; + } + } + if (idx != mbRegisterBlocksCount) { + Log.warningln("MB_GPIO %d: Mismatch in Modbus block count. Expected %d, populated %d.", id, mbRegisterBlocksCount, idx); + } + mbRegisterBlocksCount = idx; // Use actual populated count + mbBlockView.count = idx; +} + +// ----------------------------------------------------------------------------- +// Load method implementation - Can now modify internal pinConfigs +// ----------------------------------------------------------------------------- +inline short MB_GPIO::load(const JsonObject &config) +{ + Log.verboseln("MB_GPIO::load - ID: %d", id); + + JsonArray pinsArray = config["pins"].as(); + if (!pinsArray) + { + Log.warningln("MB_GPIO %d: JSON config missing 'pins' array or it's not an array. Clearing existing config.", id); + pinConfigs.clear(); // Clear internal vector + } + else + { + Log.infoln("MB_GPIO %d: Loading %zu pin configurations from JSON...", id, pinsArray.size()); + pinConfigs.clear(); // Clear internal vector before loading new ones + pinConfigs.reserve(pinsArray.size()); // Reserve capacity + + for (JsonObject pinConfigJson : pinsArray) + { + // --- Parse individual pin config --- + E_GPIO_Pin pinNum = (E_GPIO_Pin)pinConfigJson["pinNumber"].as(); // Required + E_GPIO_Type pinType = (E_GPIO_Type)pinConfigJson["pinType"].as(); // Required + + ushort addr = pinConfigJson["modbusAddress"] | 0xFFFF; // Use default invalid address + E_FN_CODE fn = (E_FN_CODE)(pinConfigJson["modbusFunction"] | (int)E_FN_CODE::FN_NONE); + E_ModbusAccess acc = (E_ModbusAccess)(pinConfigJson["modbusAccess"] | (int)MB_ACCESS_NONE); + uint32_t interval = pinConfigJson["opIntervalMs"] | 100; // Default interval + const char *name = pinConfigJson["name"] | nullptr; + const char *group = pinConfigJson["group"] | nullptr; + + // Basic validation (example) + if (pinNum < E_GPIO_0 || pinNum > E_GPIO_48) + { + Log.errorln("MB_GPIO %d: Invalid pinNumber %d in config.", id, (int)pinNum); + continue; // Skip this pin config + } + + pinConfigs.emplace_back(pinNum, pinType, addr, fn, acc, interval, name, group); // Use emplace_back + } + } + + allocateAndPopulateMbBlocks(); // Re-create Modbus blocks from the new internal pinConfigs + setup(); // Re-apply pin modes based on the new internal pinConfigs + + Log.infoln("MB_GPIO %d: Load complete. %d pins configured, %d Modbus blocks prepared.", id, pinConfigs.size(), mbRegisterBlocksCount); + return E_OK; +} + +// ----------------------------------------------------------------------------- +// setup / loop --------------------------------------------------------------- +// ----------------------------------------------------------------------------- +inline short MB_GPIO::setup() +{ + for (const auto &cfg : pinConfigs) // Use direct member access + { + configurePinModeInternal(cfg.pinNumber, cfg.pinType); + } + return E_OK; +} + +inline short MB_GPIO::loop() +{ + Component::loop(); + return E_OK; +} + +inline short MB_GPIO::info(short flags, short val) +{ + Log.verboseln("MB_GPIO::info – ID %d, pins %d, NetCaps 0x%04X", id, (int)pinConfigs.size(), nFlags); // Use direct access + for (size_t i = 0; i < pinConfigs.size(); ++i) // Use direct access + { + const auto &cfg = pinConfigs[i]; // Use direct access + Log.verboseln(" [%d] Pin %d, Type %d, MB %u, Access %d, Intv %u, Last %ld, Cached %d", + (int)i, (int)cfg.pinNumber, (int)cfg.pinType, cfg.startAddress, (int)cfg.access, + cfg.opIntervalMs, cfg.lastOpTimestamp, cfg.cachedValue); + } + for(int i = 0; i < mbRegisterBlocksCount; ++i) { + Log.verboseln(" - Block %d: Addr=%u, Cnt=%u, Type=%u, Acc=%u, Name=%s", i, mbRegisterBlocksData[i].startAddress, mbRegisterBlocksData[i].count, mbRegisterBlocksData[i].type, mbRegisterBlocksData[i].access, mbRegisterBlocksData[i].name ? mbRegisterBlocksData[i].name : "null"); + } + return E_OK; +} + +inline short MB_GPIO::serial_register(Bridge *bridge) +{ + Component::serial_register(bridge); + bridge->registerMemberFunction(id, this, C_STR("info"), (ComponentFnPtr)&MB_GPIO::info); + return E_OK; +} + +// ----------------------------------------------------------------------------- +// Modbus read / write --------------------------------------------------------- +// ----------------------------------------------------------------------------- +inline short MB_GPIO::mb_tcp_read(short address) +{ + if (!hasNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS)) + return E_INVALID_PARAMETER; + + const auto *cfg = findConfigByModbusAddress(address); // Uses internal pinConfigs + if (!cfg){ + return 0; + } + if (cfg->access == MB_ACCESS_READ_ONLY || cfg->access == MB_ACCESS_READ_WRITE) + { + return readPinInternal(cfg->pinNumber, cfg->pinType); + } + return E_OK; +} + +inline short MB_GPIO::mb_tcp_write(short address, short networkValue) +{ + if (!hasNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS)) + return E_INVALID_PARAMETER; + + GPIO_PinConfig *cfg = findConfigByModbusAddress(address); + if (!cfg) + return E_INVALID_PARAMETER; + + if (cfg->access == MB_ACCESS_WRITE_ONLY || cfg->access == MB_ACCESS_READ_WRITE) + { + return writePinInternal(*cfg, networkValue); // Pass the non-const config + } + Log.warningln("MB_GPIO %d: MB Write Addr %u (Pin %d) - Access denied (%d)", id, cfg->startAddress, static_cast(cfg->pinNumber), static_cast(cfg->access)); + return E_INVALID_PARAMETER; +} + +inline void MB_GPIO::mb_tcp_register(ModbusTCP *manager) const +{ + if (!hasNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS) || !manager) + return; + if (!mbRegisterBlocksData || mbRegisterBlocksCount == 0) + return; + Component *self = const_cast(this); + for (int i = 0; i < mbRegisterBlocksCount; ++i) + { + manager->registerModbus(self, mbRegisterBlocksData[i]); + } +} + +// ----------------------------------------------------------------------------- +// Helper to set pin‑mode ------------------------------------------------------ +// ----------------------------------------------------------------------------- +inline short MB_GPIO::configurePinModeInternal(E_GPIO_Pin pin, E_GPIO_Type type) +{ + switch (type) + { + case E_GPIO_TYPE_INPUT: + pinMode((int)pin, INPUT); + return E_OK; + case E_GPIO_TYPE_OUTPUT: + pinMode((int)pin, OUTPUT); + return E_OK; + case E_GPIO_TYPE_INPUT_PULLUP: + pinMode((int)pin, INPUT_PULLUP); + return E_OK; + case E_GPIO_TYPE_OUTPUT_OPEN_DRAIN: + pinMode((int)pin, OUTPUT_OPEN_DRAIN); + return E_OK; + case E_GPIO_TYPE_INPUT_PULLDOWN: + return E_NOT_IMPLEMENTED; + case E_GPIO_TYPE_ANALOG_INPUT: + case E_GPIO_TYPE_TOUCH: + return E_OK; + default: + Log.errorln("Unknown MB_GPIO type %d for pin %d", (int)type, (int)pin); + return E_INVALID_PARAMETER; + } +} +/* +// ----------------------------------------------------------------------------- +// Static map of pin capabilities (unchanged – omitted here for brevity) +// ----------------------------------------------------------------------------- +// Define static capabilities map (if still needed) +inline const std::map MB_GPIO::pinCapabilities = { + {E_GPIO_0, CAP_RTC | CAP_ADC1 | CAP_TOUCH | CAP_CLK_OUT3}, + {E_GPIO_1, CAP_RTC | CAP_ADC1 | CAP_TOUCH | CAP_CLK_OUT2}, + {E_GPIO_2, CAP_RTC | CAP_ADC1 | CAP_TOUCH | CAP_CLK_OUT1}, + {E_GPIO_3, CAP_RTC | CAP_ADC1 | CAP_TOUCH | CAP_JTAG_TDI | CAP_JTAG_TDO}, // Pinout shows TDO, schematic might show TDI + {E_GPIO_4, CAP_RTC | CAP_ADC1 | CAP_TOUCH | CAP_JTAG_TCK}, + {E_GPIO_5, CAP_RTC | CAP_ADC1 | CAP_TOUCH | CAP_FSPI_CS0}, + {E_GPIO_6, CAP_RTC | CAP_ADC1 | CAP_TOUCH | CAP_FSPI_CLK}, + {E_GPIO_7, CAP_RTC | CAP_ADC1 | CAP_TOUCH | CAP_FSPI_MISO}, + {E_GPIO_8, CAP_RTC | CAP_ADC1 | CAP_TOUCH | CAP_I2C0_SDA}, + {E_GPIO_9, CAP_RTC | CAP_ADC1 | CAP_TOUCH | CAP_I2C0_SCL | CAP_FSPI_HD}, + {E_GPIO_10, CAP_RTC | CAP_ADC2 | CAP_TOUCH | CAP_FSPI_WP}, + {E_GPIO_11, CAP_RTC | CAP_ADC2 | CAP_TOUCH | CAP_FSPI_IO6}, + {E_GPIO_12, CAP_RTC | CAP_ADC2 | CAP_TOUCH | CAP_FSPI_IO7}, + {E_GPIO_13, CAP_RTC | CAP_ADC2 | CAP_TOUCH | CAP_FSPI_IO5}, + {E_GPIO_14, CAP_RTC | CAP_ADC2 | CAP_TOUCH | CAP_FSPI_IO4}, + {E_GPIO_15, CAP_RTC | CAP_ADC2 | CAP_UART0_RX}, // TOUCH? No. ADC2_5. XTAL_P. RTS0. + {E_GPIO_16, CAP_RTC | CAP_ADC2 | CAP_UART0_TX}, // TOUCH? No. ADC2_6. XTAL_N. CTS0. + {E_GPIO_17, CAP_RTC | CAP_ADC2 | CAP_UART1_RX}, // ADC2_7? No ADC2_6. RXD1 + {E_GPIO_18, CAP_RTC | CAP_ADC2 | CAP_UART1_TX}, // ADC2_7. TXD1 + {E_GPIO_19, CAP_RTC | CAP_ADC2 | CAP_USB_D_MINUS | CAP_CLK_OUT2 | CAP_UART2_RX}, // ADC2_8. RTS1. + {E_GPIO_20, CAP_RTC | CAP_ADC2 | CAP_USB_D_PLUS | CAP_CLK_OUT1 | CAP_UART2_TX}, // ADC2_9. CTS1. + {E_GPIO_21, CAP_RTC}, + {E_GPIO_35, CAP_RTC | CAP_ADC1}, // ADC1_7. + {E_GPIO_36, CAP_RTC | CAP_ADC1 | CAP_FSPI_IO7}, // ADC1_8. + {E_GPIO_37, CAP_RTC | CAP_ADC1 | CAP_FSPI_IO6}, // ADC1_9. + {E_GPIO_38, CAP_RTC | CAP_FSPI_WP | CAP_LED_BUILTIN}, + {E_GPIO_39, CAP_RTC | CAP_CLK_OUT3 | CAP_JTAG_TCK}, // MTCK + {E_GPIO_40, CAP_RTC | CAP_CLK_OUT2 | CAP_JTAG_TDO}, // MTDO + {E_GPIO_41, CAP_RTC | CAP_CLK_OUT1 | CAP_JTAG_TDI}, // MTDI + {E_GPIO_42, CAP_RTC | CAP_JTAG_TMS}, // MTMS + {E_GPIO_43, CAP_UART0_TX}, + {E_GPIO_44, CAP_UART0_RX}, + {E_GPIO_45, CAP_NONE}, // VSPI, Strapping + {E_GPIO_46, CAP_NONE}, // Strapping + {E_GPIO_47, CAP_NONE}, // SPICLK_P + {E_GPIO_48, CAP_NONE} // SPICLK_N, RGB LED +}; +*/ +#endif // GPIO_H diff --git a/src/components/Joystick.cpp b/src/components/Joystick.cpp new file mode 100644 index 00000000..b9330020 --- /dev/null +++ b/src/components/Joystick.cpp @@ -0,0 +1,270 @@ +#include "Joystick.h" +#include +#include + +// Define F() macro wrapper for non-AVR architectures if needed +#if defined(ARDUINO_ARCH_AVR) + #define JOY_L(FSTR) F(FSTR) +#else + #define JOY_L(FSTR) (FSTR) +#endif + +// Constructor +Joystick::Joystick(Component *owner, + ushort _pinUp, + ushort _pinDown, + ushort _pinLeft, + ushort _pinRight, + ushort _modbusAddress) + : Component("Joystick", 100, Component::COMPONENT_DEFAULT, owner), + pinUp (_pinUp), + pinDown (_pinDown), + pinLeft (_pinLeft), + pinRight (_pinRight), + modbusAddr (_modbusAddress), + currentPosition (E_POSITION::CENTER), + lastPosition (E_POSITION::CENTER), + mode (E_MODE::LOCAL), + overridePosition (E_POSITION::CENTER), + positionStartMs (0), + lastReadMs (0), + proposedPosition (E_POSITION::CENTER), + confirmCount (0), + useDebouncing (true), + modbusBlockCount (0) +{ + setNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS); + + Log.verboseln(JOY_L("Joystick ID:%d: Created. Pins: Up:%d Down:%d Left:%d Right:%d"), + id, pinUp, pinDown, pinLeft, pinRight); + + // Setup ModbusBlocks + memset(modbusBlocks, 0, sizeof(modbusBlocks)); + const char *joyGroup = "Joystick - 4P"; + + // Position register + modbusBlocks[0] = { + static_cast(modbusAddr + static_cast(E_REGISTER::POSITION)), + 1, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, + static_cast(id), static_cast(E_REGISTER::POSITION), + "Position", joyGroup }; + + // Mode register (read/write) + modbusBlocks[1] = { + static_cast(modbusAddr + static_cast(E_REGISTER::MODE)), + 1, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_WRITE, + static_cast(id), static_cast(E_REGISTER::MODE), + "Mode", joyGroup }; + + // Override register (read/write) + modbusBlocks[2] = { + static_cast(modbusAddr + static_cast(E_REGISTER::OVERRIDE)), + 1, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_WRITE, + static_cast(id), static_cast(E_REGISTER::OVERRIDE), + "Override", joyGroup }; + + modbusBlockCount = 3; + modbusView.data = modbusBlocks; + modbusView.count = modbusBlockCount; +} + +// Initialize component +short Joystick::setup() { + Component::setup(); + + // Configure pins as inputs with pull-up resistors + pinMode(pinUp, INPUT_PULLUP); + pinMode(pinDown, INPUT_PULLUP); + pinMode(pinLeft, INPUT_PULLUP); + pinMode(pinRight, INPUT_PULLUP); + + return reset(); +} + +// Reset component state +short Joystick::reset() { + // Reset state + currentPosition = readPinsPosition(); + lastPosition = currentPosition; + proposedPosition = currentPosition; + confirmCount = DEBOUNCE_COUNT; + positionStartMs = now; + + Log.verboseln(JOY_L("Joystick ID:%d: Reset. Position: %d"), + id, static_cast(currentPosition)); + return E_OK; +} + +// Read joystick position from GPIO pins +Joystick::E_POSITION Joystick::readPinsPosition() { + // Note: Pull-up means LOW when pressed/active + bool up = !digitalRead(pinUp); + bool down = !digitalRead(pinDown); + bool left = !digitalRead(pinLeft); + bool right = !digitalRead(pinRight); + + // Detect invalid combinations (opposing directions pressed simultaneously) + if ((up && down) || (left && right)) { + Log.verboseln(JOY_L("Joystick ID:%d: Invalid input detected U:%d D:%d L:%d R:%d"), + id, up, down, left, right); + return E_POSITION::UNKNOWN; + } + + // Determine joystick position based on pin states + // Simple 4-way logic + if (up) return E_POSITION::UP; + if (down) return E_POSITION::DOWN; + if (left) return E_POSITION::LEFT; + if (right) return E_POSITION::RIGHT; + + // No buttons pressed - CENTER position + return E_POSITION::CENTER; +} + +// Main loop +short Joystick::loop() { + Component::loop(); + + // Check if it's time to read pins + if (now - lastReadMs < READ_INTERVAL_MS) return E_OK; + lastReadMs = now; + + // Only process input changes if in LOCAL mode + if (mode == E_MODE::LOCAL) { + E_POSITION candidatePosition = readPinsPosition(); + + // Apply debouncing if enabled + if (useDebouncing) { + if (candidatePosition == proposedPosition) { + if (confirmCount < DEBOUNCE_COUNT) ++confirmCount; + } else { + proposedPosition = candidatePosition; + confirmCount = 1; + } + + // Update position only when debouncing confirms it + if (confirmCount >= DEBOUNCE_COUNT && proposedPosition != currentPosition) { + lastPosition = currentPosition; + currentPosition = proposedPosition; + positionStartMs = now; + Log.verboseln(JOY_L("Joystick ID:%d: Position %d → %d (debounced)"), + id, static_cast(lastPosition), static_cast(currentPosition)); + notifyStateChange(); + } + } else { + // Direct position update without debouncing + if (candidatePosition != currentPosition) { + lastPosition = currentPosition; + currentPosition = candidatePosition; + positionStartMs = now; + Log.verboseln(JOY_L("Joystick ID:%d: Position %d → %d (direct)"), + id, static_cast(lastPosition), static_cast(currentPosition)); + notifyStateChange(); + } + } + } + + return E_OK; +} + +// Info display +short Joystick::info(short, short) { + Log.infoln(JOY_L("Joystick::info - ID:%d"), id); + Log.infoln(JOY_L(" Pins - Up:%d Down:%d Left:%d Right:%d"), + pinUp, pinDown, pinLeft, pinRight); + Log.infoln(JOY_L(" Mode: %s, Debouncing: %s"), + mode == E_MODE::LOCAL ? "LOCAL" : "REMOTE", + useDebouncing ? "ENABLED" : "DISABLED"); + Log.infoln(JOY_L(" Current Position: %d, Last Position: %d"), + static_cast(currentPosition), static_cast(lastPosition)); + Log.infoln(JOY_L(" Position Holding Time: %lu ms"), getHoldingTime()); + + if (mode == E_MODE::REMOTE) { + Log.infoln(JOY_L(" Override Position: %d"), static_cast(overridePosition)); + } + return E_OK; +} + +// Notify of state changes +void Joystick::notifyStateChange() { + Component::notifyStateChange(); +} + +// Modbus write implementation +short Joystick::mb_tcp_write(MB_Registers *reg, short networkValue) { + ushort addr = reg->startAddress; + + // Handle MODE register + if (addr == modbusAddr + static_cast(E_REGISTER::MODE)) { + if (networkValue < 0 || networkValue > 1) { + Log.warningln(JOY_L("Joystick ID:%d: Invalid mode value: %d"), id, networkValue); + return MODBUS_ERROR_ILLEGAL_DATA_VALUE; + } + setMode(static_cast(networkValue)); + Log.verboseln(JOY_L("Joystick ID:%d: Mode set to %s"), + id, networkValue == 0 ? "LOCAL" : "REMOTE"); + return E_OK; + } + + // Handle OVERRIDE register + if (addr == modbusAddr + static_cast(E_REGISTER::OVERRIDE)) { + if (networkValue < 0 || networkValue > static_cast(E_POSITION::UNKNOWN)) { + Log.warningln(JOY_L("Joystick ID:%d: Invalid override position: %d"), id, networkValue); + return MODBUS_ERROR_ILLEGAL_DATA_VALUE; + } + setOverridePosition(static_cast(networkValue)); + Log.verboseln(JOY_L("Joystick ID:%d: Override position set to %d"), id, networkValue); + return E_OK; + } + + // Handle POSITION register (read-only) + if (addr == modbusAddr + static_cast(E_REGISTER::POSITION)) { + Log.warningln(JOY_L("Joystick ID:%d: Write to read-only address %d"), id, addr); + return MODBUS_ERROR_ILLEGAL_FUNCTION; + } + + Log.warningln(JOY_L("Joystick ID:%d: Write to unknown address %d"), id, addr); + return MODBUS_ERROR_ILLEGAL_DATA_ADDRESS; +} + +// Modbus read implementation +short Joystick::mb_tcp_read(MB_Registers *reg) { + ushort addr = reg->startAddress; + + // Current position (accounting for mode) + if (addr == modbusAddr + static_cast(E_REGISTER::POSITION)) { + return static_cast(getValue()); + } + + // Mode (LOCAL/REMOTE) + if (addr == modbusAddr + static_cast(E_REGISTER::MODE)) { + return static_cast(mode); + } + + // Override position value + if (addr == modbusAddr + static_cast(E_REGISTER::OVERRIDE)) { + return static_cast(overridePosition); + } + + Log.warningln(JOY_L("Joystick ID:%d: Read from unknown address %d"), id, addr); + return 0; +} + +// Register with ModbusTCP manager +void Joystick::mb_tcp_register(ModbusTCP *mgr) const { + auto *blocks = mb_tcp_blocks(); + if (!blocks || !blocks->data) return; + Component *thiz = const_cast(this); + for (ushort i = 0; i < blocks->count; ++i) mgr->registerModbus(thiz, blocks->data[i]); +} + +// Return Modbus blocks +ModbusBlockView *Joystick::mb_tcp_blocks() const { + return const_cast(&modbusView); +} + +// Register with serial bridge +short Joystick::serial_register(Bridge *bridge) { + bridge->registerMemberFunction(id, this, C_STR("info"), (ComponentFnPtr)&Joystick::info); + return E_OK; +} \ No newline at end of file diff --git a/src/components/Joystick.h b/src/components/Joystick.h new file mode 100644 index 00000000..515a5c98 --- /dev/null +++ b/src/components/Joystick.h @@ -0,0 +1,104 @@ +#ifndef JOYSTICK_H +#define JOYSTICK_H + +#include +#include "../config.h" +#include +#include +#include "modbus/ModbusTCP.h" +#include "config-modbus.h" +#include + +class Bridge; + +class Joystick : public Component +{ +public: + enum class E_POSITION : ushort { + CENTER = 0, + UP, + DOWN, + LEFT, + RIGHT, + UNKNOWN + }; + + enum class E_MODE : ushort { + LOCAL = 0, + REMOTE = 1 + }; + + enum class E_REGISTER : ushort { + POSITION = 0, + MODE = 1, + OVERRIDE = 2 + }; + +private: + const ushort pinUp; + const ushort pinDown; + const ushort pinLeft; + const ushort pinRight; + const ushort modbusAddr; + + E_POSITION currentPosition; + E_POSITION lastPosition; + E_MODE mode; + E_POSITION overridePosition; + + // Position timing + unsigned long positionStartMs = 0; + + // Debouncing data + unsigned long lastReadMs = 0; + E_POSITION proposedPosition = E_POSITION::CENTER; + ushort confirmCount = 0; + bool useDebouncing = true; + + // Modbus definitions + MB_Registers modbusBlocks[3]; + ushort modbusBlockCount = 0; + ModbusBlockView modbusView; + + // Private helpers + E_POSITION readPinsPosition(); + +public: + Joystick( + Component *owner, + ushort _pinUp, + ushort _pinDown, + ushort _pinLeft, + ushort _pinRight, + ushort _modbusAddress = 100); + + short setup() override; + short loop() override; + short reset(); + short info(short val0 = 0, short val1 = 0) override; + short debug() override { return info(0, 0); } + + E_POSITION getPosition() const { return mode == E_MODE::LOCAL ? currentPosition : overridePosition; } + ushort getValue() const { return static_cast(getPosition()); } + E_POSITION getLastPosition() const { return lastPosition; } + unsigned long getHoldingTime() const { return now - positionStartMs; } + E_MODE getMode() const { return mode; } + + void setMode(E_MODE _mode) { mode = _mode; notifyStateChange(); } + void setOverridePosition(E_POSITION pos) { overridePosition = pos; if (mode == E_MODE::REMOTE) notifyStateChange(); } + + short mb_tcp_write(MB_Registers *reg, short networkValue) override; + short mb_tcp_read(MB_Registers *reg) override; + void mb_tcp_register(ModbusTCP *manager) const override; + ModbusBlockView *mb_tcp_blocks() const override; + short serial_register(Bridge *bridge) override; + +protected: + void notifyStateChange() override; + + // Configuration + static constexpr ushort DEBOUNCE_COUNT = 3; + static constexpr ushort READ_INTERVAL_MS = 25; +}; + +#endif // JOYSTICK_H \ No newline at end of file diff --git a/src/components/LEDFeedback.cpp b/src/components/LEDFeedback.cpp new file mode 100644 index 00000000..fa33abc8 --- /dev/null +++ b/src/components/LEDFeedback.cpp @@ -0,0 +1,339 @@ +#include "LEDFeedback.h" +#include // For millis(), pinMode potentially (though NeoPixel lib handles pin) +#include // For memset + +// Define update interval and fade increment locally for now +// These could be moved to config.h if needed elsewhere +const unsigned long LED_UPDATE_INTERVAL_MS = 20; // ms -> ~50 FPS update rate +const float FADE_INCREMENT = 0.01f; // Controls the speed of the fade (smaller = slower) +const unsigned long TRI_COLOR_BLINK_INTERVAL_MS = 500; // Blink interval for TRI_COLOR_BLINK mode + +// Define colors for TRI_COLOR_BLINK mode (Traffic Light: Red, Yellow, Green) +const uint32_t TRI_COLOR_SECTION1 = Adafruit_NeoPixel::Color(255, 0, 0); // Red +const uint32_t TRI_COLOR_SECTION2 = Adafruit_NeoPixel::Color(255, 255, 0); // Yellow +const uint32_t TRI_COLOR_SECTION3 = Adafruit_NeoPixel::Color(0, 255, 0); // Green + +LEDFeedback::LEDFeedback( + Component *owner, + ushort _pin, + ushort _pixelCount, + ushort _id, + ushort _modbusAddress) : Component("LEDFeedback", _id, Component::COMPONENT_DEFAULT, owner), + m_pin(_pin), + m_pixelCount(_pixelCount > 0 ? _pixelCount : 1), // Ensure at least 1 pixel + m_modbusAddr(_modbusAddress), + // Initialize NeoPixel strip object. + // Type is NEO_GRB + NEO_KHZ800, common for WS2812B/SK6812 + // Adjust if using different pixel types (e.g., NEO_RGB, NEO_KHZ400) + m_strip(_pixelCount > 0 ? _pixelCount : 1, _pin, NEO_GRB + NEO_KHZ800), + m_mode(LEDMode::OFF), // Default to OFF + m_lastUpdateMs(0), + m_fadeProgress(0.0f), + m_fadingUp(false), + m_rangeLevel(0), // Initialize range level + m_triColorBlinkStateOn(true), // Default to on state + m_lastBlinkToggleMs(0), + m_modbusBlockCount(0) // Initialize actual count +{ + setNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS); + + if (_pixelCount == 0) { + Log.warningln("LEDFeedback ID:%d: pixelCount is 0, defaulting to 1.", id); + } + + // Define Modbus blocks + memset(m_modbusBlocks, 0, sizeof(m_modbusBlocks)); // Clear array + int blockIndex = 0; + + // Register 0: Mode Control + if (blockIndex < MAX_LED_FEEDBACK_REGISTERS) { + m_modbusBlocks[blockIndex++] = { + static_cast(m_modbusAddr + static_cast(LEDRegOffset::MODE)), + 1, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_WRITE, // Mode is R/W + static_cast(id), static_cast(LEDRegOffset::MODE), + "LED Mode", "LED" + }; + } + // Register 1: Level Control for RANGE mode + if (blockIndex < MAX_LED_FEEDBACK_REGISTERS) { + m_modbusBlocks[blockIndex++] = { + static_cast(m_modbusAddr + static_cast(LEDRegOffset::LEVEL)), + 1, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_WRITE, // Level is R/W + static_cast(id), static_cast(LEDRegOffset::LEVEL), + "LED Level", "LED" + }; + } + // Add other registers (e.g., Brightness) here later if needed + + m_modbusBlockCount = blockIndex; // Set the actual number of registers used + + // Set up the view + m_modbusView.data = m_modbusBlocks; + m_modbusView.count = m_modbusBlockCount; +} + +short LEDFeedback::setup() { + Component::setup(); + m_strip.begin(); // Initialize NeoPixel library + m_strip.clear(); // Initialize all pixels to 'off' + m_strip.setBrightness(50); // Set moderate initial brightness (0-255), adjust as needed + m_strip.show(); // Send data to pixels + m_lastUpdateMs = millis(); // Initialize timestamp + Log.verboseln("LEDFeedback ID:%d setup. Pin:%d, Pixels:%d, Mode:%d, MBAddr:%d", + id, m_pin, m_pixelCount, static_cast(m_mode), m_modbusAddr); + return E_OK; +} + +void LEDFeedback::handleModeOff() { + // Optimization: Check if strip is already off? getPixelColor(0) might work. + // For simplicity, just clear and show. + m_strip.clear(); + m_strip.show(); +} + +void LEDFeedback::handleModeFadeRB() { + // Update fade progress + if (m_fadingUp) { + m_fadeProgress += FADE_INCREMENT; + if (m_fadeProgress >= 1.0f) { + m_fadeProgress = 1.0f; + m_fadingUp = false; // Change direction + } + } else { + m_fadeProgress -= FADE_INCREMENT; + if (m_fadeProgress <= 0.0f) { + m_fadeProgress = 0.0f; + m_fadingUp = true; // Change direction + } + } + // Calculate color and fill strip + m_strip.fill(calculateFadeColor()); + m_strip.show(); // Update the strip +} + +void LEDFeedback::handleModeRange() { + m_strip.clear(); // Clear all pixels first + + // Calculate how many pixels to light + // Ensure m_pixelCount is not zero to avoid division by zero, though constructor ensures it's at least 1. + ushort pixelsToLight = static_cast((m_rangeLevel / 100.0f) * m_pixelCount); + if (pixelsToLight > m_pixelCount) pixelsToLight = m_pixelCount; // Cap at max + + // Define color for RANGE mode (e.g., Green) - could be configurable + uint32_t rangeColor = m_strip.Color(0, 255, 0); // Green + + for (ushort i = 0; i < pixelsToLight; ++i) { + m_strip.setPixelColor(i, rangeColor); + } + m_strip.show(); +} + +void LEDFeedback::handleModeTriColorBlink() { + unsigned long now = millis(); // Use Component::now if available and preferred + + // Check if it's time to toggle blink state + if (now - m_lastBlinkToggleMs >= TRI_COLOR_BLINK_INTERVAL_MS) { + m_triColorBlinkStateOn = !m_triColorBlinkStateOn; + m_lastBlinkToggleMs = now; + } + + if (m_triColorBlinkStateOn) { + ushort pixelsPerSection = m_pixelCount / 3; + ushort section1End = pixelsPerSection; + ushort section2End = 2 * pixelsPerSection; + + for (ushort i = 0; i < m_pixelCount; ++i) { + if (i < section1End) { + m_strip.setPixelColor(i, TRI_COLOR_SECTION1); + } else if (i < section2End) { + m_strip.setPixelColor(i, TRI_COLOR_SECTION2); + } else { + m_strip.setPixelColor(i, TRI_COLOR_SECTION3); + } + } + } else { + m_strip.clear(); + } + m_strip.show(); +} + +short LEDFeedback::loop() { + Component::loop(); + unsigned long now = millis(); // Use Component::now if available and preferred + + // Allow updates slightly more frequently if needed, but base logic on interval + if (now - m_lastUpdateMs < LED_UPDATE_INTERVAL_MS) { + return E_OK; // Not time to update yet + } + m_lastUpdateMs = now; + + switch (m_mode) { + case LEDMode::OFF: + handleModeOff(); + break; + + case LEDMode::FADE_R_B: + handleModeFadeRB(); + break; + + case LEDMode::RANGE: + handleModeRange(); + break; + + case LEDMode::TRI_COLOR_BLINK: + handleModeTriColorBlink(); + break; + + // Add cases for future modes here + default: + // Unknown mode, default to OFF + if (m_mode != LEDMode::OFF) { + Log.warningln("LEDFeedback ID:%d: Unknown mode %d, turning OFF.", id, static_cast(m_mode)); + m_mode = LEDMode::OFF; // Ensure state consistency + } + handleModeOff(); // Default to OFF behavior + break; + } + + return E_OK; +} + +// Helper to calculate intermediate color for fade +uint32_t LEDFeedback::calculateFadeColor() const { + uint8_t r1 = (m_color1 >> 16) & 0xFF; + uint8_t g1 = (m_color1 >> 8) & 0xFF; + uint8_t b1 = m_color1 & 0xFF; + uint8_t r2 = (m_color2 >> 16) & 0xFF; + uint8_t g2 = (m_color2 >> 8) & 0xFF; + uint8_t b2 = m_color2 & 0xFF; + + float progress = m_fadeProgress; // Use current progress + // Ensure progress stays within [0.0, 1.0] if FADE_INCREMENT logic has edge cases + if (progress < 0.0f) progress = 0.0f; + if (progress > 1.0f) progress = 1.0f; + + uint8_t r = static_cast(r1 * (1.0f - progress) + r2 * progress); + uint8_t g = static_cast(g1 * (1.0f - progress) + g2 * progress); + uint8_t b = static_cast(b1 * (1.0f - progress) + b2 * progress); + + return m_strip.Color(r, g, b); +} + + +short LEDFeedback::info(short val0, short val1) { + Log.infoln("LEDFeedback::info - ID: %d", id); + Log.infoln(" Pin: %d, Pixel Count: %d", m_pin, m_pixelCount); + Log.infoln(" Modbus Addr: %d, Block Count: %d", m_modbusAddr, m_modbusBlockCount); + const char* modeStr = "UNKNOWN"; + switch(m_mode) { + case LEDMode::OFF: modeStr = "OFF"; break; + case LEDMode::FADE_R_B: modeStr = "FADE_R_B"; break; + case LEDMode::RANGE: modeStr = "RANGE"; break; + case LEDMode::TRI_COLOR_BLINK: modeStr = "TRI_COLOR_BLINK"; break; + } + Log.infoln(" Current Mode: %d (%s)", static_cast(m_mode), modeStr); + if (m_mode == LEDMode::FADE_R_B) { + Log.infoln(" Fade Progress: %.2f, Fading Up: %s", m_fadeProgress, m_fadingUp ? "true" : "false"); + } + if (m_mode == LEDMode::RANGE) { + Log.infoln(" Range Level: %d", m_rangeLevel); + } + // Log Modbus block details if helpful + // for(ushort i=0; istartAddress; + + if (regAddr == static_cast(baseAddr + static_cast(LEDRegOffset::MODE))) { + LEDMode newMode = static_cast(networkValue); + + // Validate the new mode (add future valid modes to this check) + if (newMode == LEDMode::OFF || newMode == LEDMode::FADE_R_B || newMode == LEDMode::RANGE || newMode == LEDMode::TRI_COLOR_BLINK) { + if (newMode != m_mode) { + Log.verboseln("LEDFeedback ID:%d: Mode change %d -> %d", id, static_cast(m_mode), static_cast(newMode)); + m_mode = newMode; + // Reset mode-specific state when mode changes + m_fadeProgress = 0.0f; + m_fadingUp = false; + // Reset blink state for TRI_COLOR_BLINK + m_triColorBlinkStateOn = true; + m_lastBlinkToggleMs = millis(); + m_lastUpdateMs = millis(); // Force immediate update in new mode + notifyStateChange(); // Notify app/others if needed + } + return E_OK; + } else { + Log.warningln("LEDFeedback ID:%d: Invalid mode value written: %d", id, networkValue); + return MODBUS_ERROR_ILLEGAL_DATA_VALUE; + } + } else if (regAddr == static_cast(baseAddr + static_cast(LEDRegOffset::LEVEL))) { + if (networkValue >= 0 && networkValue <= 100) { + if (static_cast(networkValue) != m_rangeLevel) { + Log.verboseln("LEDFeedback ID:%d: Level change %d -> %d", id, m_rangeLevel, networkValue); + m_rangeLevel = static_cast(networkValue); + m_lastUpdateMs = millis(); // Force update if mode is RANGE + notifyStateChange(); + } + return E_OK; + } else { + Log.warningln("LEDFeedback ID:%d: Invalid level value written: %d (must be 0-100)", id, networkValue); + return MODBUS_ERROR_ILLEGAL_DATA_VALUE; + } + } + // Handle other writable registers here later + + Log.warningln("LEDFeedback ID:%d: Write attempt to unhandled Modbus address %d.", id, regAddr); + return MODBUS_ERROR_ILLEGAL_DATA_ADDRESS; +} + +short LEDFeedback::mb_tcp_read(MB_Registers *reg) { + if (!reg || m_modbusBlockCount == 0) return 0; // Or error code + + ushort baseAddr = m_modbusAddr; + ushort regAddr = reg->startAddress; + + if (regAddr == static_cast(baseAddr + static_cast(LEDRegOffset::MODE))) { + return static_cast(m_mode); + } + if (regAddr == static_cast(baseAddr + static_cast(LEDRegOffset::LEVEL))) { + return static_cast(m_rangeLevel); + } + // Handle other readable registers here later + + Log.warningln("LEDFeedback ID:%d: Read attempt from unhandled Modbus address %d.", id, regAddr); + return 0; +} + +void LEDFeedback::mb_tcp_register(ModbusTCP *manager) const { + if (!manager || !hasNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS) || m_modbusBlockCount == 0) { + return; + } + ModbusBlockView *blocksView = mb_tcp_blocks(); + Component *thiz = const_cast(this); + if (blocksView && blocksView->data) { + for (int i = 0; i < blocksView->count; ++i) { + MB_Registers info = blocksView->data[i]; + manager->registerModbus(thiz, info); + } + } +} + +ModbusBlockView *LEDFeedback::mb_tcp_blocks() const { + return const_cast(&m_modbusView); +} + +short LEDFeedback::serial_register(Bridge *bridge) { + if (!bridge) return E_INVALID_PARAMETER; + bridge->registerMemberFunction(id, this, C_STR("info"), (ComponentFnPtr)&LEDFeedback::info); + return E_OK; +} + +void LEDFeedback::notifyStateChange() { + Component::notifyStateChange(); + // Currently called when mode changes via Modbus +} \ No newline at end of file diff --git a/src/components/LEDFeedback.h b/src/components/LEDFeedback.h new file mode 100644 index 00000000..260acccb --- /dev/null +++ b/src/components/LEDFeedback.h @@ -0,0 +1,110 @@ +#ifndef LED_FEEDBACK_H +#define LED_FEEDBACK_H + +#include +#include "../config.h" // For LED_UPDATE_INTERVAL_MS potentially +#include // If needed by Component or others +#include +#include // NeoPixel library +#include "../modbus/ModbusTCP.h" // For Modbus interaction base +#include "../config-modbus.h" // For Modbus addresses/constants + +// Forward declare if necessary +class Bridge; + +// Define max registers needed (Mode + potential future params like Brightness) +#define MAX_LED_FEEDBACK_REGISTERS 3 + +class LEDFeedback : public Component { +public: + // Define modes for the LED strip + enum class LEDMode : ushort { + OFF = 0, + FADE_R_B = 1, // Fade Red <-> Blue + RANGE = 2, // Display a level 0-100 + TRI_COLOR_BLINK = 3 // Three sections blinking + // Add more modes later, e.g., SOLID_COLOR, RAINBOW + }; + + // Define Modbus register offsets relative to base address + enum class LEDRegOffset : ushort { + MODE = 0, + LEVEL = 1, // For RANGE mode level (0-100) + // BRIGHTNESS = 2 // Add later if needed + }; + +private: + // General Configuration & State + const ushort m_pin; // NeoPixel data pin + const ushort m_pixelCount; // Number of pixels in the strip + const ushort m_modbusAddr; // Base Modbus address + Adafruit_NeoPixel m_strip; // NeoPixel Object + LEDMode m_mode = LEDMode::OFF; // Current active mode + unsigned long m_lastUpdateMs = 0; // Timestamp of the last general LED update + + // Modbus Definitions + MB_Registers m_modbusBlocks[MAX_LED_FEEDBACK_REGISTERS]; + ushort m_modbusBlockCount = 0; + ModbusBlockView m_modbusView; + + // --- LEDMode::FADE_R_B --- + // State + float m_fadeProgress = 0.0f; // 0.0 to 1.0 for fade + bool m_fadingUp = false; // Direction of fade (false = C1->C2, true = C2->C1) + // Colors (could be made configurable later) + const uint32_t m_color1 = Adafruit_NeoPixel::Color(255, 0, 0); // Red + const uint32_t m_color2 = Adafruit_NeoPixel::Color(0, 0, 255); // Blue + // Helper + uint32_t calculateFadeColor() const; + void handleModeFadeRB(); + + // --- LEDMode::RANGE --- + // State + ushort m_rangeLevel = 0; // Level for RANGE mode (0-100) + // Helper + void handleModeRange(); + + // --- LEDMode::TRI_COLOR_BLINK --- + // State + bool m_triColorBlinkStateOn = true; + unsigned long m_lastBlinkToggleMs = 0; + // Helper + void handleModeTriColorBlink(); + + // General Private Helpers + void handleModeOff(); // Handles LEDMode::OFF + +public: + LEDFeedback( + Component *owner, + ushort _pin, + ushort _pixelCount, + ushort _id, + ushort _modbusAddress); + + ~LEDFeedback() override = default; // Default destructor is likely fine + + // Component Lifecycle + short setup() override; + short loop() override; + short info(short val0 = 0, short val1 = 0) override; + short debug() override { return info(0, 0); } + + // Mode Control (optional direct methods) + // void setMode(LEDMode newMode); + // LEDMode getMode() const { return m_mode; } + + // Modbus Interface + short mb_tcp_write(MB_Registers *reg, short networkValue) override; + short mb_tcp_read(MB_Registers *reg) override; + void mb_tcp_register(ModbusTCP *manager) const override; + ModbusBlockView *mb_tcp_blocks() const override; + + // Serial Interface + short serial_register(Bridge *bridge) override; + +protected: + void notifyStateChange() override; // Override if mode change notification is useful +}; + +#endif // LED_FEEDBACK_H \ No newline at end of file diff --git a/src/components/ModbusLogicEngine.cpp b/src/components/ModbusLogicEngine.cpp new file mode 100644 index 00000000..4d4000a2 --- /dev/null +++ b/src/components/ModbusLogicEngine.cpp @@ -0,0 +1,417 @@ +// Include config first to get the feature flag +#include "config.h" + +#ifdef ENABLE_MB_SCRIPT + +#include "ModbusLogicEngine.h" +#include "PHApp.h" // Include PHApp to access other components/methods +#include "ModbusTCP.h" // To potentially interact with Modbus directly if needed +#include "config-modbus.h" // Assuming constants like MODBUS_LOGIC_RULES_START are here +#include "enums.h" // For error codes like E_OK, component states etc. +#include // For millis() +#include // For isnan, isinf if needed +#include // For strncpy +#include + +// Placeholder for Component ID - Define this properly elsewhere (e.g., enums.h) +#ifndef COMPONENT_ID_MLE +#define COMPONENT_ID_MLE 99 +#endif + +// --- Constructor --- +ModbusLogicEngine::ModbusLogicEngine(PHApp* ownerApp) + // Using the most complete constructor signature found in error logs + // Assuming 0 for default flags + : Component("LogicEngine", COMPONENT_ID_MLE, 0, ownerApp), app(ownerApp), initialized_(false) { // Initialize internal state flag + // Name, ID, Flags, Owner set in base initializer list +} + +// --- Setup --- +short ModbusLogicEngine::setup() { + Log.infoln(F("MLE: Initializing Logic Engine...")); + rules.resize(MAX_LOGIC_RULES); // Allocate space for all rules + + // Optional: Load rules from persistent storage if implemented + + Log.infoln(F("MLE: Initialized %d rules."), MAX_LOGIC_RULES); + // Set internal initialization flag + initialized_ = true; + return E_OK; +} + +// --- Loop (Rule Evaluation) --- +short ModbusLogicEngine::loop() { + if (!initialized_) { + return E_OK; // Not ready to run + } + + unsigned long currentTime = millis(); + if (currentTime - lastLoopTime < loopInterval) { + return E_OK; // Not time to evaluate yet + } + lastLoopTime = currentTime; + + //Log.verboseln(F("MLE: Evaluating rules...")); + + for (int i = 0; i < rules.size(); ++i) { + LogicRule& rule = rules[i]; + + if (!rule.isEnabled()) { + continue; // Skip disabled rules + } + + bool conditionMet = false; + bool conditionEvalSuccess = evaluateCondition(rule); + + if (!conditionEvalSuccess) { + // Error already logged and status updated in evaluateCondition + if(rule.isDebugEnabled()) Log.verboseln(F("MLE: Rule %d condition eval FAILED."), i); + continue; // Move to the next rule + } + + conditionMet = conditionEvalSuccess; // If evaluation didn't fail, the result is stored + + if (conditionMet) { + if(rule.isDebugEnabled()) Log.verboseln(F("MLE: Rule %d condition MET."), i); + bool actionSuccess = performAction(rule); + if (actionSuccess) { + // Status updated in performAction + // Timestamp and counter are also updated in performAction + if(rule.isReceiptEnabled()) Log.infoln(F("MLE: Rule %d action successful."), i); + } else { + // Error logged and status updated in performAction + if(rule.isDebugEnabled()) Log.warningln(F("MLE: Rule %d action FAILED."), i); + } + } else { + // Condition not met + if(rule.isDebugEnabled()) Log.verboseln(F("MLE: Rule %d condition NOT met."), i); + // Ensure status is reset to OK/Idle if it wasn't an error + if (rule.lastStatus != RuleStatus::IllegalDataAddress && + rule.lastStatus != RuleStatus::IllegalDataValue) { + updateRuleStatus(rule, RuleStatus::Success); + } + } + } // End for each rule + + return E_OK; +} + +// --- Evaluate Condition --- +bool ModbusLogicEngine::evaluateCondition(LogicRule& rule) { + uint16_t currentValue = 0; + uint16_t targetValue = rule.getCondValue(); + RegisterState::E_RegType sourceType = rule.getCondSourceType(); + uint16_t sourceAddr = rule.getCondSourceAddr(); + ConditionOperator op = rule.getCondOperator(); + + // Read the source value + bool readSuccess = readConditionSourceValue(sourceType, sourceAddr, currentValue); + if (!readSuccess) { + Log.warningln(F("MLE: Failed to read condition source (Type: %d, Addr: %u)"), (int)sourceType, sourceAddr); + updateRuleStatus(rule, RuleStatus::IllegalDataAddress); + return false; // Indicate evaluation failure + } + + if (rule.isDebugEnabled()) { + Log.verboseln(F("MLE Eval [%d]: SrcType=%d, SrcAddr=%u, Op=%d, Target=%u, Current=%u"), + &rule - &rules[0], // Crude way to get index for logging + (int)sourceType, sourceAddr, (int)op, targetValue, currentValue); + } + + // Perform comparison + bool result = false; + switch (op) { + case ConditionOperator::EQUAL: result = (currentValue == targetValue); break; + case ConditionOperator::NOT_EQUAL: result = (currentValue != targetValue); break; + case ConditionOperator::LESS_THAN: result = (currentValue < targetValue); break; + case ConditionOperator::LESS_EQUAL: result = (currentValue <= targetValue); break; + case ConditionOperator::GREATER_THAN: result = (currentValue > targetValue); break; + case ConditionOperator::GREATER_EQUAL: result = (currentValue >= targetValue); break; + default: + Log.warningln(F("MLE: Invalid condition operator (%d)"), (int)op); + updateRuleStatus(rule, RuleStatus::IllegalDataValue); + return false; // Indicate evaluation failure + } + + // If we got here, evaluation itself succeeded, return the comparison result + // Status will be updated later based on whether action is performed + return result; +} + +// --- Perform Action (Renamed Command) --- +bool ModbusLogicEngine::performAction(LogicRule& rule) { // Renamed function + CommandType commandType = rule.getCommandType(); // Renamed enum and accessor + uint16_t target = rule.getCommandTarget(); // Renamed accessor + uint16_t param1 = rule.getCommandParam1(); // Renamed accessor + uint16_t param2 = rule.getCommandParam2(); // Renamed accessor + bool success = false; + + if (rule.isDebugEnabled()) { + Log.verboseln(F("MLE Action [%d]: CmdType=%d, Target=%u, P1=%u, P2=%u"), + &rule - &rules[0], // Crude index for logging + (int)commandType, target, param1, param2); + } + + switch (commandType) { + case CommandType::NONE: + success = true; // No action is considered success + break; + case CommandType::WRITE_HOLDING_REGISTER: + case CommandType::WRITE_COIL: + success = performWriteAction(commandType, target, param1); + if (!success) updateRuleStatus(rule, RuleStatus::ServerDeviceFailure); + break; + case CommandType::CALL_COMPONENT_METHOD: + success = performCallAction(target, param1, 0); + if (!success) updateRuleStatus(rule, RuleStatus::OpExecutionFailed); + break; + default: + Log.warningln(F("MLE: Invalid command type (%d)"), (int)commandType); + updateRuleStatus(rule, RuleStatus::IllegalFunction); + success = false; + break; + } + + // Update timestamp and counter only on successful action execution + if (success) { + updateRuleStatus(rule, RuleStatus::Success); + rule.lastTriggerTimestamp = millis() / 1000; // Store seconds since boot + rule.triggerCount++; + } + + return success; +} + +// --- Read Condition Source Value --- +bool ModbusLogicEngine::readConditionSourceValue(RegisterState::E_RegType type, uint16_t address, uint16_t& value) { + if (!app || !app->modbusManager) { + Log.errorln(F("MLE: ModbusManager not available!")); + return false; + } + + // Find the component responsible for this address + Component* targetComponent = app->modbusManager->findComponentForAddress(address); + if (!targetComponent) { + Log.warningln(F("MLE: No component found for read address %u"), address); + // Optionally: Check if it's a global app address? + if (app->modbusManager->findComponentForAddress(address) == static_cast(app)){ + targetComponent = static_cast(app); + } else { + return false; + } + } + + short result = -1; + switch (type) { + case RegisterState::E_RegType::REG_HOLDING: + case RegisterState::E_RegType::REG_INPUT: + result = targetComponent->mb_tcp_read(address); + break; + case RegisterState::E_RegType::REG_COIL: + case RegisterState::E_RegType::REG_DISCRETE_INPUT: + result = targetComponent->mb_tcp_read(address); + break; + default: + Log.warningln(F("MLE: Unsupported condition source type: %d"), (int)type); + return false; + } + + if (result == E_INVALID_PARAMETER || result < -1) { + Log.warningln(F("MLE: Read failed for address %u (Result: %d)"), address, result); + return false; + } + + // Handle potential E_NOT_IMPLEMENTED or other non-value returns if necessary + if (result == E_NOT_IMPLEMENTED) { + Log.warningln(F("MLE: Read not implemented for address %u"), address); + return false; + } + + value = (uint16_t)result; + Log.verboseln(F("MLE: Read condition value %u from address %u"), value, address); + return true; +} + +// --- Perform Write Action --- +bool ModbusLogicEngine::performWriteAction(CommandType type, uint16_t address, uint16_t value) { + if (!app || !app->modbusManager) { + Log.errorln(F("MLE: ModbusManager not available!")); + return false; + } + + // Find the component responsible for this address + Component* targetComponent = app->modbusManager->findComponentForAddress(address); + if (!targetComponent) { + Log.warningln(F("MLE: No component found for write address %u"), address); + // Optionally: Check if it's a global app address? + if (app->modbusManager->findComponentForAddress(address) == static_cast(app)){ + targetComponent = static_cast(app); + } else { + return false; + } + } + + Log.verboseln(F("MLE: Writing value %u to address %u (Type: %d)"), value, address, (int)type); + + // Assuming mb_tcp_write handles both Registers and Coils + // It should return E_OK on success + short result = targetComponent->mb_tcp_write(address, value); + + if (result != E_OK) { + Log.warningln(F("MLE: Write failed for address %u, value %u (Result: %d)"), address, value, result); + return false; + } + + return true; +} + +// --- Perform Call Action --- +bool ModbusLogicEngine::performCallAction(uint16_t componentId, uint16_t methodId, uint16_t arg1) { + uint32_t combinedId = ((uint32_t)componentId << 16) | methodId; + auto it = callableMethods.find(combinedId); + + // Find the rule being processed - This requires passing the rule or index down + int ruleIndex = -1; // Placeholder index + // Logic to determine the correct ruleIndex based on loop context is needed here. + // For now, just check if index is valid before accessing rules vector. + if (ruleIndex < 0 || ruleIndex >= rules.size()) { + Log.errorln(F("MLE: Invalid rule context in performCallAction!")); + // Cannot update status without rule context + return false; + } + LogicRule& rule = rules[ruleIndex]; // Now safe to access + + if (it == callableMethods.end()) { + Log.warningln(F("MLE: Method not registered (CompID: %u, MethodID: %u)"), componentId, methodId); + updateRuleStatus(rule, RuleStatus::IllegalDataAddress); + return false; + } + + Log.verboseln(F("MLE: Calling method (CompID: %u, MethodID: %u) with arg (%d)"), componentId, methodId, (short)arg1); + // Pass only arg1 (param2 from rule) to the registered function, assuming it now takes only one argument + // OR pass arg1 and a dummy second argument if the registered function still expects two. + // Let's assume the registered CallableMethod now expects only one argument + // TODO: Confirm signature of registered CallableMethod + // short result = it->second((short)arg1, (short)arg2); // Old call + short result = it->second((short)arg1, 0); // Pass arg1 and a dummy 0 for now + + if (result != E_OK) { // Check against generic E_OK from enums.h + Log.warningln(F("MLE: Method call failed (CompID: %u, MethodID: %u, Result: %d)"), componentId, methodId, result); + // Status is updated in performAction based on return + return false; + } + + return true; +} + +// --- Register Method --- +bool ModbusLogicEngine::registerMethod(uint16_t componentId, uint16_t methodId, CallableMethod method) { + uint32_t combinedId = ((uint32_t)componentId << 16) | methodId; + auto result = callableMethods.insert({combinedId, method}); + + if (result.second) { + Log.infoln(F("MLE: Registered method (CompID: %u, MethodID: %u)"), componentId, methodId); + } else { + Log.warningln(F("MLE: Failed to register duplicate method (CompID: %u, MethodID: %u)"), componentId, methodId); + } + return result.second; // Returns true if insertion took place +} + +// --- Modbus Network Interface --- + +// Helper to get rule index and offset from a Modbus address +bool ModbusLogicEngine::getRuleInfoFromAddress(short address, int& ruleIndex, short& offset) { + if (address < MODBUS_LOGIC_RULES_START) { + return false; + } + short relativeAddress = address - MODBUS_LOGIC_RULES_START; + ruleIndex = relativeAddress / LOGIC_ENGINE_REGISTERS_PER_RULE; + offset = relativeAddress % LOGIC_ENGINE_REGISTERS_PER_RULE; + + if (ruleIndex < 0 || ruleIndex >= MAX_LOGIC_RULES) { + return false; // Address out of range + } + return true; +} + +// Read Configuration/Status via Modbus +short ModbusLogicEngine::mb_tcp_read(short address) { + int ruleIndex = -1; + short offset = -1; + + if (!getRuleInfoFromAddress(address, ruleIndex, offset)) { + return E_INVALID_PARAMETER; + } + + const LogicRule& rule = rules[ruleIndex]; + + // Read configuration registers (Offset 0 to 8) + if (offset >= ModbusLogicEngineOffsets::ENABLED && offset <= ModbusLogicEngineOffsets::COMMAND_PARAM2) { + return rule.config[offset]; + } + // Read status/flag registers (Offset 9 to 12) + else if (offset == ModbusLogicEngineOffsets::FLAGS) { + return rule.getFlags(); // Use getFlags() to read from config array + } + else if (offset == ModbusLogicEngineOffsets::LAST_STATUS) { + // Return MB_Error as short + return static_cast(rule.lastStatus); + } + else if (offset == ModbusLogicEngineOffsets::LAST_TRIGGER_TS) { + // Timestamps might be 32-bit, Modbus registers are 16-bit. + // Return lower 16 bits for simplicity, or implement 32-bit reading. + return (short)(rule.lastTriggerTimestamp & 0xFFFF); // Return lower 16 bits + // TODO: Implement reading upper 16 bits at offset + 1 if needed + } + else if (offset == ModbusLogicEngineOffsets::TRIGGER_COUNT) { + return rule.triggerCount; + } + else { + return E_INVALID_PARAMETER; + } +} + +// Write Configuration via Modbus +short ModbusLogicEngine::mb_tcp_write(short address, short value) { + int ruleIndex = -1; + short offset = -1; + + if (!getRuleInfoFromAddress(address, ruleIndex, offset)) { + return E_INVALID_PARAMETER; + } + + LogicRule& rule = rules[ruleIndex]; + + // Allow writing to configuration registers (Offset 0 to 9, including FLAGS) + if (offset >= ModbusLogicEngineOffsets::ENABLED && offset <= ModbusLogicEngineOffsets::FLAGS) { + Log.verboseln(F("MLE: Setting Rule %d, Offset %d to %d"), ruleIndex, offset, value); + rule.setConfigValue(offset, (uint16_t)value); + // Optional: Persist rule change here if implementing storage + return E_OK; + } + // Allow resetting trigger count (Offset 12) + else if (offset == ModbusLogicEngineOffsets::TRIGGER_COUNT && value == 0) { + Log.infoln(F("MLE: Resetting trigger count for Rule %d"), ruleIndex); + rule.triggerCount = 0; + return E_OK; + } + // Disallow writing to other status registers directly (Offsets 10, 11) + else if (offset == ModbusLogicEngineOffsets::LAST_STATUS || offset == ModbusLogicEngineOffsets::LAST_TRIGGER_TS) { + Log.warningln(F("MLE: Attempt to write read-only status register (Rule %d, Offset %d)"), ruleIndex, offset); + return E_INVALID_PARAMETER; + } + else { + return E_INVALID_PARAMETER; + } +} + +// --- Helper to update status (and log changes) --- +void ModbusLogicEngine::updateRuleStatus(LogicRule& rule, RuleStatus newStatus) { + if (rule.lastStatus != newStatus) { + Log.verboseln(F("MLE: Rule Status changing from %d to %d"), static_cast(rule.lastStatus), static_cast(newStatus)); + rule.lastStatus = newStatus; + // Optional: Log specific error messages based on the status code + } +} + +#endif // ENABLE_MB_SCRIPT \ No newline at end of file diff --git a/src/components/ModbusLogicEngine.h b/src/components/ModbusLogicEngine.h new file mode 100644 index 00000000..f37e46e4 --- /dev/null +++ b/src/components/ModbusLogicEngine.h @@ -0,0 +1,200 @@ +#ifndef MODBUS_LOGIC_ENGINE_H +#define MODBUS_LOGIC_ENGINE_H + +#include "config.h" + +#ifdef ENABLE_MB_SCRIPT + +#include +#include +#include +#include // For uint16_t etc. +#include "modbus/ModbusTypes.h" + +// Forward declarations +class PHApp; // Assuming PHApp provides access to other components/Modbus + +// --- Configuration Constants (Define these in config-modbus.h or similar) --- +#ifndef MAX_LOGIC_RULES +#define MAX_LOGIC_RULES 8 // Default number of rules +#endif + +#ifndef LOGIC_ENGINE_REGISTERS_PER_RULE +#define LOGIC_ENGINE_REGISTERS_PER_RULE 13 // Now 13 (removed Param3) +#endif + +#ifndef MODBUS_LOGIC_RULES_START +#define MODBUS_LOGIC_RULES_START 1000 // Example starting address +#endif +// --- End Configuration Constants --- + +// Define constants for rule structure offsets (matching mb-lang.md) +namespace ModbusLogicEngineOffsets { + // --- Configuration (8 registers) --- + const short ENABLED = 0; + const short COND_SRC_TYPE = 1; + const short COND_SRC_ADDR = 2; + const short COND_OPERATOR = 3; + const short COND_VALUE = 4; + const short COMMAND_TYPE = 5; + const short COMMAND_TARGET = 6; + // Parameters below depend on COMMAND_TYPE: + // - WRITE_HOLDING_REGISTER: PARAM1=Value + // - WRITE_COIL: PARAM1=Value (0 or 1) + // - CALL_COMPONENT_METHOD: PARAM1=MethodID, PARAM2=Arg1 + const short COMMAND_PARAM1 = 7; + const short COMMAND_PARAM2 = 8; + // Removed PARAM3 + + // --- Status & Flags (Moved to end - 4 registers) --- + const short FLAGS = 9; // Moved + const short LAST_STATUS = 10; // Moved + const short LAST_TRIGGER_TS = 11; // Moved + const short TRIGGER_COUNT = 12; // Moved +} + +// --- Rule Flags (Bitmasks for FLAGS register) --- +#define RULE_FLAG_DEBUG (1 << 0) // Enable verbose debug logging for this rule +#define RULE_FLAG_RECEIPT (1 << 1) // Enable logging upon successful trigger/action + +// Define constants for condition operators +enum class ConditionOperator : uint16_t { + EQUAL = 0, + NOT_EQUAL = 1, + LESS_THAN = 2, + LESS_EQUAL = 3, + GREATER_THAN = 4, + GREATER_EQUAL = 5 +}; + +// Define constants for command types +// Partially aligned with E_MB_OpType from ModbusTypes.h +enum class CommandType : uint16_t { + NONE = 0, + // Use standard op types where possible (Values from E_MB_OpType) + WRITE_COIL = 2, // Matches E_MB_OpType::MB_WRITE_COIL + WRITE_HOLDING_REGISTER = 3, // Matches E_MB_OpType::MB_WRITE_REGISTER + // Custom command type (value > standard op types) + CALL_COMPONENT_METHOD = 100 +}; + +// Type alias for rule status using standard Modbus errors +using RuleStatus = MB_Error; + +// Structure to hold the configuration and state of a single rule +struct LogicRule { + // --- Configuration (Set via Modbus) --- + // Store config registers directly (Enabled to Param2) + uint16_t config[LOGIC_ENGINE_REGISTERS_PER_RULE - 4]; // Size now 13-4 = 9 + + // Helper accessors for configuration + bool isEnabled() const { return config[ModbusLogicEngineOffsets::ENABLED] == 1; } + RegisterState::E_RegType getCondSourceType() const { return static_cast(config[ModbusLogicEngineOffsets::COND_SRC_TYPE]); } + uint16_t getCondSourceAddr() const { return config[ModbusLogicEngineOffsets::COND_SRC_ADDR]; } + ConditionOperator getCondOperator() const { return static_cast(config[ModbusLogicEngineOffsets::COND_OPERATOR]); } + uint16_t getCondValue() const { return config[ModbusLogicEngineOffsets::COND_VALUE]; } + CommandType getCommandType() const { return static_cast(config[ModbusLogicEngineOffsets::COMMAND_TYPE]); } + uint16_t getCommandTarget() const { return config[ModbusLogicEngineOffsets::COMMAND_TARGET]; } + uint16_t getCommandParam1() const { return config[ModbusLogicEngineOffsets::COMMAND_PARAM1]; } + uint16_t getCommandParam2() const { return config[ModbusLogicEngineOffsets::COMMAND_PARAM2]; } + + // --- Status & Flags (Read/Write via Modbus, updated internally) --- + // Note: These are conceptually separate but stored contiguously in Modbus address space. + // The internal representation uses separate members for status/timestamp/count + // and reads/writes the FLAGS register (config[9]) via Modbus. + uint16_t getFlags() const { return config[ModbusLogicEngineOffsets::FLAGS]; } + bool isDebugEnabled() const { return (getFlags() & RULE_FLAG_DEBUG) != 0; } + bool isReceiptEnabled() const { return (getFlags() & RULE_FLAG_RECEIPT) != 0; } + + void setConfigValue(short offset, uint16_t value) { + // Size check updated for new register count (9 config registers) + if (offset >= 0 && offset < (LOGIC_ENGINE_REGISTERS_PER_RULE - 4)) { + config[offset] = value; + } else { + Log.warningln(F("MLE: Attempt to write invalid config offset %d"), offset); + } + } + + // Internal state members (not directly part of config array) + RuleStatus lastStatus = RuleStatus::Success; + uint32_t lastTriggerTimestamp = 0; + uint16_t triggerCount = 0; + + LogicRule() { + // Initialize configuration registers (0-8) + for(int i = 0; i < (LOGIC_ENGINE_REGISTERS_PER_RULE - 4); ++i) { + config[i] = 0; + } + } +}; + + +class ModbusLogicEngine : public Component { +public: + // Define the callable method signature: takes two shorts, returns a short (e.g., error code) + // Using std::function allows storing lambdas, member functions, etc. + // NOTE: This internal registration is used for CALL_COMPONENT_METHOD. + // It is separate from the Bridge mechanism used for serial commands. + using CallableMethod = std::function; + + // Constructor - Requires access to PHApp to interact with other components/Modbus + ModbusLogicEngine(PHApp* ownerApp); // Pass PHApp reference + + virtual ~ModbusLogicEngine() = default; + + short setup() override; + short loop() override; + + // Handle Modbus reads/writes for rule configuration and status + short mb_tcp_read(short address) override; + short mb_tcp_write(short address, short value) override; + + /** + * @brief Registers a method that can be called by the logic engine. + * + * @param componentId A unique ID for the component exposing the method. + * @param methodId A unique ID for the method within that component. + * @param method The function object (lambda, bound member function) to call. + * @return True if registration was successful, false otherwise (e.g., duplicate ID). + */ + bool registerMethod(uint16_t componentId, uint16_t methodId, CallableMethod method); + +private: + PHApp* app; // Pointer to the main application instance + bool initialized_; // Flag to track initialization state + + // Storage for all logic rules + std::vector rules; + + // Registry for callable methods: Map<(ComponentID << 16) | MethodID, Function> + // Combining IDs into a single 32-bit key for the map. + std::map callableMethods; + + // --- Internal Logic --- + unsigned long lastLoopTime = 0; + const unsigned long loopInterval = 100; // Evaluate rules every 100ms (adjust as needed) + + bool evaluateCondition(LogicRule& rule); + bool performAction(LogicRule& rule); + + // --- Helper Methods --- + // Reads the value required for a rule's condition + // Updated signature to use RegisterState::E_RegType + bool readConditionSourceValue(RegisterState::E_RegType type, uint16_t address, uint16_t& value); + // Performs the write action (Register or Coil) + // Updated signature to use CommandType + bool performWriteAction(CommandType type, uint16_t address, uint16_t value); + // Performs the method call action (now only 2 params) + bool performCallAction(uint16_t componentId, uint16_t methodId, uint16_t arg1); + + // Helper to calculate rule index and offset from Modbus address + bool getRuleInfoFromAddress(short address, int& ruleIndex, short& offset); + + // Helper to update status fields + // Updated signature to use RuleStatus (MB_Error) + void updateRuleStatus(LogicRule& rule, RuleStatus status); +}; + +#endif // ENABLE_MB_SCRIPT + +#endif // MODBUS_LOGIC_ENGINE_H \ No newline at end of file diff --git a/src/components/OmronE5.cpp b/src/components/OmronE5.cpp new file mode 100644 index 00000000..9640ff64 --- /dev/null +++ b/src/components/OmronE5.cpp @@ -0,0 +1,840 @@ +#include "config.h" + +#include +#include "error_codes.h" +#include "components/OmronE5.h" +#include "components/OmronE5Types.h" +#include "modbus/ModbusTypes.h" +#include "modbus/Modbus.h" +#include "RS485.h" +#include "constants.h" +#include + +#define HEATUP_DEADBAND 15 // Fixed deadband value for heatup detection, percentage +#define OMRON_E5_SP_MAX 260 // Maximum allowed Set Point value + +// --- Cooling Feature Setup Instructions --- +// To enable cooling monitoring: +// 1. Uncomment the #define ENABLE_COOLING above. +// 2. Ensure the Omron E5 controller is configured for Heat/Cool control +// (Register 0x2D11 = 1) and has an output assigned to cooling +// (e.g., Register 0x2E06 or 0x2E07 = 2) via its own settings menu +// or separate configuration tool. This code only reads the cooling MV. +// --- End Setup Instructions --- + +#ifdef ENABLE_RS485 + +enum class E_ExecuteCommands : short { + INFO = 1, + RESET_STATS = 2 +}; + +enum class E_OmronTcpOffset : ushort { + PV = 1, + STATUS_HIGH = 2, + STATUS_LOW = 3, +#ifdef ENABLE_TRUTH_COLLECTOR + MEAN_ERROR = 4, + SP = 5, + HEAT_RATE = 6, + WH_LOW = 7, + WH_HIGH = 8, + PV_SP_LAG = 9, // Tenths of PV Unit per minute (PV SP Lag) + TOTAL_COST_CENTS = 10, // Estimated total cost in Euro Cents + LONGEST_HEAT_60S = 15, // Longest heating duration in last 60s (seconds) +#else + SP = 5, +#endif + CMD_SP = 11, + CMD_STOP = 12, + CMD_EXECUTE = 13, // Write 1 for info, 2 for reset stats + IS_HEATING = 14, // Read-only status: 1 if heating, 0 otherwise + IS_HEATUP = 16 // Read-only status: 1 if PV < SP - Deadband +}; + +OmronE5::OmronE5(uint8_t slaveId, millis_t readInterval) : RTU_Base(slaveId, COMPONENT_KEY::COMPONENT_KEY_PID + slaveId), + _readInterval(readInterval), + _pvValid(false), + _spValid(false), + _statusValid(false) +{ + componentId = id; + syncInterval = readInterval; + name = "OmronE5[" + String(slaveId) + "]"; + setNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS); + const uint16_t tcpBaseAddr = OMRON_MB_TCP_OFFSET + (this->slaveId * OMRON_TCP_REG_RANGE); + _modbusBlocks[0] = INIT_MODBUS_BLOCK(E_OmronTcpOffset::PV, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, "OmronE5: PV", name.c_str()); + _modbusBlocks[1] = INIT_MODBUS_BLOCK(E_OmronTcpOffset::SP, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, "OmronE5: SP", name.c_str()); + _modbusBlocks[2] = INIT_MODBUS_BLOCK(E_OmronTcpOffset::STATUS_LOW, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, "OmronE5: Status Low", name.c_str()); + _modbusBlocks[3] = INIT_MODBUS_BLOCK(E_OmronTcpOffset::STATUS_HIGH, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, "OmronE5: Status High", name.c_str()); + _modbusBlocks[4] = INIT_MODBUS_BLOCK(E_OmronTcpOffset::CMD_SP, E_FN_CODE::FN_WRITE_HOLD_REGISTER, MB_ACCESS_READ_WRITE, "OmronE5: Set Point Command (Reg)", name.c_str()); + _modbusBlocks[5] = INIT_MODBUS_BLOCK(E_OmronTcpOffset::CMD_STOP, E_FN_CODE::FN_WRITE_COIL, MB_ACCESS_READ_WRITE, "OmronE5: Run/Stop Coil", name.c_str()); +#ifdef ENABLE_TRUTH_COLLECTOR + _modbusBlocks[6] = INIT_MODBUS_BLOCK(E_OmronTcpOffset::MEAN_ERROR, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, "OmronE5: Mean Error (0-100)", name.c_str()); + _modbusBlocks[7] = INIT_MODBUS_BLOCK(E_OmronTcpOffset::HEAT_RATE, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, "OmronE5: Heat Rate (osc/min)", name.c_str()); + _modbusBlocks[8] = INIT_MODBUS_BLOCK(E_OmronTcpOffset::WH_LOW, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, "OmronE5: Total Wh (Low)", name.c_str()); + _modbusBlocks[9] = INIT_MODBUS_BLOCK(E_OmronTcpOffset::WH_HIGH, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, "OmronE5: Total Wh (High)", name.c_str()); + _modbusBlocks[10] = INIT_MODBUS_BLOCK(E_OmronTcpOffset::PV_SP_LAG, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, "OmronE5: PV SP Lag (tenths/min)", name.c_str()); + _modbusBlocks[11] = INIT_MODBUS_BLOCK(E_OmronTcpOffset::CMD_EXECUTE, E_FN_CODE::FN_WRITE_HOLD_REGISTER, MB_ACCESS_READ_WRITE, "OmronE5: Execute Command", name.c_str()); + _modbusBlocks[12] = INIT_MODBUS_BLOCK(E_OmronTcpOffset::TOTAL_COST_CENTS, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, "OmronE5: Total Cost (Cents)", name.c_str()); + _modbusBlocks[13] = INIT_MODBUS_BLOCK(E_OmronTcpOffset::IS_HEATING, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, "OmronE5: Heating Status", name.c_str()); + _modbusBlocks[14] = INIT_MODBUS_BLOCK(E_OmronTcpOffset::LONGEST_HEAT_60S, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, "OmronE5: Longest Heat (s)", name.c_str()); + _modbusBlocks[15] = INIT_MODBUS_BLOCK(E_OmronTcpOffset::IS_HEATUP, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, "OmronE5: Heatup Status", name.c_str()); +#else + _modbusBlocks[6] = INIT_MODBUS_BLOCK(E_OmronTcpOffset::CMD_EXECUTE_INFO, E_FN_CODE::FN_WRITE_HOLD_REGISTER, MB_ACCESS_READ_WRITE, "OmronE5: Execute Info Command", name.c_str()); + _modbusBlocks[7] = INIT_MODBUS_BLOCK(E_OmronTcpOffset::IS_HEATING, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, "OmronE5: Heating Status", name.c_str()); +#ifndef ENABLE_TRUTH_COLLECTOR // Add IS_HEATUP block only if not already added + _modbusBlocks[8] = INIT_MODBUS_BLOCK(E_OmronTcpOffset::IS_HEATUP, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, "OmronE5: Heatup Status", name.c_str()); +#endif +#endif +// Add static_assert *after* potential conditional block definitions +static_assert(sizeof(_modbusBlocks)/sizeof(_modbusBlocks[0]) == OmronE5::OMRON_TCP_BLOCK_COUNT, "Mismatch in _modbusBlocks size and OMRON_TCP_BLOCK_COUNT"); + _modbusBlockView = {_modbusBlocks, OMRON_TCP_BLOCK_COUNT}; +} + +uint16_t OmronE5::mb_tcp_base_address() const +{ + // Calculate and return the base TCP address for this specific Omron E5 instance + return OMRON_MB_TCP_OFFSET + (this->slaveId * OMRON_TCP_REG_RANGE); +} + +uint16_t OmronE5::mb_tcp_offset_for_rtu_address(uint16_t rtuAddress) const +{ + switch (rtuAddress) + { + case OMRON_E5_READ_BLOCK_START_ADDR + 1: // PV (likely 0x0001) + return static_cast(E_OmronTcpOffset::PV); + case OMRON_E5_READ_BLOCK_START_ADDR + 2: // Status High (likely 0x0002) + return static_cast(E_OmronTcpOffset::STATUS_HIGH); + case OMRON_E5_READ_BLOCK_START_ADDR + 3: // Status Low (likely 0x0003) + return static_cast(E_OmronTcpOffset::STATUS_LOW); + case OMRON_E5_READ_BLOCK_START_ADDR + 5: // SP (likely 0x0005) + return static_cast(E_OmronTcpOffset::SP); + // Add mappings for other RTU addresses if they directly correspond to TCP offsets + // For example, if writing RTU 0x2103 should update the value associated with TCP CMD_SP: + // case OR_E5_SWR_SP: // 0x2103 + // return static_cast(E_OmronTcpOffset::CMD_SP); + + // If RTU address 0x2005 (Cooling MV Low) maps to a specific TCP offset: + #ifdef ENABLE_COOLING + // case 0x2005: return static_cast(E_OmronTcpOffset::COOLING_MV_LOW); // Example offset + // case 0x2006: return static_cast(E_OmronTcpOffset::COOLING_MV_HIGH); // Example offset + #endif + + default: + // Return 0 or an invalid offset marker if the RTU address doesn't map directly + // to a defined TCP offset for broadcast purposes. + return 0; + } +} + +short OmronE5::mb_tcp_read(MB_Registers *reg) +{ + if (!reg) + { + Log.errorln(F("OmronE5[%d]::mb_tcp_read - Invalid MB_Registers pointer"), this->slaveId); + return (short)MB_Error::ServerDeviceFailure; // Or E_INVALID_PARAMETER + } + + // Use the new virtual function to get the base address for this instance + const uint16_t instanceBaseAddr = this->mb_tcp_base_address(); + if (instanceBaseAddr == 0) { // Handle cases where TCP mapping might not be configured + Log.errorln(F("OmronE5[%d]::mb_tcp_read - TCP Base Address is 0, cannot process read."), this->slaveId); + return (short)MB_Error::ServerDeviceFailure; + } + + const short requestedAddress = reg->startAddress; + // Calculate offset from the instance's base TCP address + short offset = requestedAddress - instanceBaseAddr; + + // Check if the calculated offset corresponds to a valid register offset (1 to OMRON_TCP_REG_RANGE) + if (offset < 1 || offset > OMRON_TCP_REG_RANGE) + { + Log.warningln(F("OmronE5[%d]: Received read request for address %d which maps to an invalid offset %d (Valid: 1-%d). Base: %d"), + this->slaveId, requestedAddress, offset, OMRON_TCP_REG_RANGE, instanceBaseAddr); + return (short)MB_Error::IllegalDataAddress; + } + + // Offset is now guaranteed to be valid (1 to 16) + uint16_t value = 0; + bool success = false; + // Cast the valid offset to the enum type for use in the switch + E_OmronTcpOffset regOffset = static_cast(offset); + + // Handle reads based on the specific register offset + switch (regOffset) + { + case E_OmronTcpOffset::PV: // Offset 1 + success = getPV(value); + break; + case E_OmronTcpOffset::SP: // Offset 5 + success = getSP(value); + break; + case E_OmronTcpOffset::STATUS_LOW: // Offset 3 + success = getStatusLow(value); + break; + case E_OmronTcpOffset::STATUS_HIGH: // Offset 2 + success = getStatusHigh(value); + break; +#ifdef ENABLE_TRUTH_COLLECTOR + case E_OmronTcpOffset::MEAN_ERROR: // Offset 4: Scaled Mean Error + { + float meanError = getMeanError(); + if (_errorStats.count() == 0 || isnan(meanError)) { + value = 0; // Return 0 if no data or NaN + } else { + const float MAX_ERROR_FOR_SCALING = 100.0f; + // Clamp the error between 0 and MAX_ERROR_FOR_SCALING + float clampedError = meanError < 0.0f ? 0.0f : (meanError > MAX_ERROR_FOR_SCALING ? MAX_ERROR_FOR_SCALING : meanError); + // Scale to 0-100 + value = static_cast((clampedError / MAX_ERROR_FOR_SCALING) * 100.0f); + } + success = true; // Reading the statistic is considered successful + } + break; + case E_OmronTcpOffset::HEAT_RATE: // Offset 6: Heat Rate (osc/min) + { + float rate = getHeatRate(); + value = static_cast(rate); // Cast float rate to integer + success = true; // Reading the statistic is considered successful + } + break; + case E_OmronTcpOffset::WH_LOW: // Offset 7: Total Wh Low Word + { + uint32_t totalWhInt = static_cast(_totalWh); + value = totalWhInt & 0xFFFF; // Lower 16 bits + success = true; + } + break; + case E_OmronTcpOffset::WH_HIGH: // Offset 8: Total Wh High Word + { + uint32_t totalWhInt = static_cast(_totalWh); + value = (totalWhInt >> 16) & 0xFFFF; // Upper 16 bits + success = true; + } + break; + case E_OmronTcpOffset::PV_SP_LAG: // Offset 9: PV SP Lag (Tenths/min) + value = static_cast(_pvSpLag); // Return the stored lag (cast from int16_t) + success = true; // Reading the calculated value is considered successful + break; + case E_OmronTcpOffset::TOTAL_COST_CENTS: // Offset 10: Total Cost in Cents + { + float totalKWh = static_cast(static_cast(_totalWh)) / 1000.0f; + float totalCostEuros = totalKWh * PRICE_PER_KWH_EUROS; + // Convert to cents, ensuring it fits within uint16_t (max 65535 cents = 655.35 EUR) + float totalCostCentsFloat = totalCostEuros * 100.0f; + if (totalCostCentsFloat > 65535.0f) { + value = 65535; // Clamp to max value if overflow + } else if (totalCostCentsFloat < 0.0f) { + value = 0; // Ensure non-negative + } else { + value = static_cast(totalCostCentsFloat); + } + success = true; // Calculation is considered successful + } + break; + case E_OmronTcpOffset::LONGEST_HEAT_60S: // Offset 15: Longest Heat duration in last 60s + value = getLongestHeatDuration60s(); + success = true; // Reading the tracked value is successful + break; +#endif + case E_OmronTcpOffset::CMD_SP: // Offset 11: Read associated with write SP command (returns current SP) + success = getSP(value); // Read the actual SP + break; + case E_OmronTcpOffset::CMD_STOP: // Offset 12: Read associated with Run/Stop command (returns current status) + value = isRunning() ? 1 : 0; // 1 = Running (Coil Value 0), 0 = Stopped (Coil Value 1) + success = true; + break; + case E_OmronTcpOffset::CMD_EXECUTE: // Offset 13: Reading this trigger always returns 0 + value = 0; + success = true; + break; + case E_OmronTcpOffset::IS_HEATING: // Offset 14: Heating Status + value = isHeating() ? 1 : 0; + success = true; + break; + case E_OmronTcpOffset::IS_HEATUP: // Offset 16: Heatup Status + value = isHeatup() ? 1 : 0; + success = true; // Checking the status is always considered successful + break; + + default: + // This case handles offsets that are within the allocated range [0, OMRON_TCP_REG_RANGE) + // but are not explicitly mapped to a readable value in the E_OmronTcpOffset enum. + // (e.g., offsets 0, 4, 6, 7, 8, 9, 10, 13, 14, 15 in a range of 16) + Log.warningln(F("OmronE5[%d]: Read attempt on unhandled offset %d within TCP block (Address: %d)."), this->slaveId, offset, requestedAddress); + // Treat unmapped but valid addresses within the block as illegal data addresses for read operations. + return (short)MB_Error::IllegalDataAddress; + } + + // Check if the underlying getter function succeeded (e.g., data was valid) + if (!success) + { + // Log.warningln(F("OmronE5[%d]: Failed to get valid data for TCP Address: %d, Offset: %d (underlying data invalid or unavailable)."), this->slaveId, requestedAddress, offset); + // Return a value indicating failure to retrieve valid data from the device. + // 0xFFFF is a common way to signal an error/invalid state in Modbus register reads + // when no specific Modbus error code perfectly fits. + return 0xFFFF; // Indicate data not available/valid from the source + } + + // Return the successfully retrieved value + return value; +} + +short OmronE5::mb_tcp_write(MB_Registers *reg, short value) +{ + if (!reg) + return E_INVALID_PARAMETER; + + // Use the new virtual function to get the base address for this instance + const uint16_t tcpBaseAddr = this->mb_tcp_base_address(); + if (tcpBaseAddr == 0) { // Handle cases where TCP mapping might not be configured + Log.errorln(F("OmronE5[%d]::mb_tcp_write - TCP Base Address is 0, cannot process write."), this->slaveId); + return (short)MB_Error::ServerDeviceFailure; + } + + const short requestedTcpAddress = reg->startAddress; + // Calculate offset from the instance's base TCP address + short offset = requestedTcpAddress - tcpBaseAddr; + + // Use the calculated offset (cast to enum) for checks + E_OmronTcpOffset regOffset = static_cast(offset); + + bool commandSuccess = false; + + // Check against the enum offset directly + if (regOffset == E_OmronTcpOffset::CMD_SP) + { + // Clamp the value before setting + uint16_t clampedValue = (uint16_t)value > OMRON_E5_SP_MAX ? OMRON_E5_SP_MAX : (uint16_t)value; + if (clampedValue != (uint16_t)value) { + Log.warningln(F("OmronE5[%d]: Requested SP %d clamped to %d"), this->slaveId, value, clampedValue); + } + commandSuccess = setSP(clampedValue); + } + else if (regOffset == E_OmronTcpOffset::CMD_STOP) + { + if (value) // Value 1 = Stop, Value 0 = Run + { + Log.infoln(F("OmronE5[%d]::mb_tcp_write - Received stop command via TCP Coil (Addr: %d, Offset: %d, Value: %d)"), this->slaveId, requestedTcpAddress, offset, value); + commandSuccess = stop(); + } + else + { + Log.infoln(F("OmronE5[%d]::mb_tcp_write - Received run command via TCP Coil (Addr: %d, Offset: %d, Value: %d)"), this->slaveId, requestedTcpAddress, offset, value); + commandSuccess = run(); + } + } + else if (regOffset == E_OmronTcpOffset::CMD_EXECUTE) + { + // Cast the incoming value to the command enum + E_ExecuteCommands command = static_cast(value); + + switch (command) + { + case E_ExecuteCommands::INFO: + Log.infoln(F("OmronE5[%d]::mb_tcp_write - Received info() command via TCP Register (Addr: %d, Offset: %d, Value: %d)"), this->slaveId, requestedTcpAddress, offset, value); + this->info(); // Execute the info method + commandSuccess = true; // Command execution was successful + break; +#ifdef ENABLE_TRUTH_COLLECTOR + case E_ExecuteCommands::RESET_STATS: + Log.infoln(F("OmronE5[%d]::mb_tcp_write - Received reset stats command via TCP Register (Addr: %d, Offset: %d, Value: %d)"), this->slaveId, requestedTcpAddress, offset, value); + _resetRuntimeStats(); // Execute the reset method + commandSuccess = true; // Command execution was successful + break; +#endif + default: + // Writing any other value to the execute register is not an error, just does nothing specific + Log.warningln(F("OmronE5[%d]::mb_tcp_write - Received non-command value for CMD_EXECUTE (Addr: %d, Offset: %d, Value: %d)"), this->slaveId, requestedTcpAddress, offset, value); + commandSuccess = true; // Writing a non-trigger value is also considered successful + break; + } + // Note: No persistent state change or RTU write needed for CMD_EXECUTE + } + else + { + // Handle attempt to write to a non-writable/unhandled offset + Log.warningln(F("OmronE5[%d]::mb_tcp_write - Attempt to write to unhandled or read-only TCP Address: %d (Offset: %d)"), this->slaveId, requestedTcpAddress, offset); + return (short)MB_Error::IllegalDataAddress; // Or E_ACCESS_DENIED if it's read-only + } + + if (!commandSuccess) + { + Log.errorln(F("OmronE5[%d]::mb_tcp_write - Failed to execute command for TCP Addr %d, Value %d"), this->slaveId, requestedTcpAddress, value); + return (short)MB_Error::ServerDeviceFailure; // Indicate command execution failed + } + + return (short)MB_Error::Success; // Return OK to ModbusTCP, actual write happens later +} + +ModbusBlockView *OmronE5::mb_tcp_blocks() const +{ + return const_cast(&_modbusBlockView); +} +short OmronE5::setup() +{ + Log.infoln(F("OmronE5[%d]: Setting up..."), slaveId); + // --- Mandatory Block 1: Core Status/PV/SP --- + ModbusReadBlock *block = addMandatoryReadBlock( + OMRON_E5_READ_BLOCK_START_ADDR, // 0x0000 + OMRON_E5_READ_BLOCK_REG_COUNT, // 6 registers (PV, StatusH, StatusL, SP) + E_FN_CODE::FN_READ_HOLD_REGISTER, + _readInterval); + if (block) + { + Log.infoln(F("OmronE5[%d]: Configured mandatory read block 1: Addr=0x%04X, Count=%d, Interval=%lu ms"), + slaveId, OMRON_E5_READ_BLOCK_START_ADDR, OMRON_E5_READ_BLOCK_REG_COUNT, _readInterval); + } + else + { + Log.errorln(F("OmronE5[%d]: Failed to add mandatory read block 1!"), slaveId); + return E_INVALID_PARAMETERS; + } + +#ifdef ENABLE_COOLING + // --- Mandatory Block 2: Cooling MV Monitor --- + // Read E_MV_MONITOR_COOL_REGISTER (0x2005) - Requires reading 2 registers (0x2005, 0x2006) + ModbusReadBlock *coolBlock = addMandatoryReadBlock( + 0x2005, // Start address for Cooling MV Low Word + 2, // Read 2 registers (Low and High words) + E_FN_CODE::FN_READ_HOLD_REGISTER, + _readInterval); // Use same interval for simplicity, adjust if needed + if (coolBlock) + { + Log.infoln(F("OmronE5[%d]: Configured mandatory read block 2 (Cooling MV): Addr=0x%04X, Count=%d, Interval=%lu ms"), + slaveId, 0x2005, 2, _readInterval); + } + else + { + Log.errorln(F("OmronE5[%d]: Failed to add mandatory read block 2 (Cooling MV)!"), slaveId); + // Decide if this is critical, potentially return error + // return E_INVALID_PARAMETERS; + } +#endif + + this->addOutputRegister(OR_E5_SWR_SP, E_FN_CODE::FN_WRITE_HOLD_REGISTER, 10, PRIORITY_MEDIUM); + this->addOutputRegister(OR_E5_CMD_ADDRESS::OR_E5_CMD_STOP_RUN, E_FN_CODE::FN_WRITE_COIL, 0, PRIORITY_MEDIUM); + return E_OK; +} +short OmronE5::loop() +{ + // Most of the work (scheduling reads/writes) is handled by the RS485->RTU_DeviceManager. + // The RTU_Device base class updates its internal register map when reads complete. + // This loop could be used for: + // 1. Triggering specific writes based on application logic. + // 2. Checking status flags from the read data and acting upon them. + // 3. Implementing component-specific logic not directly tied to Modbus reads/writes. + +#ifdef ENABLE_TRUTH_COLLECTOR + millis_t now = millis(); + const millis_t WINDOW_DURATION_MS = 60000 * 5; + + // --- PV Rate of Change Calculation --- + if (_pvValid) { + if (_hasPreviousPv) { + millis_t deltaTime = now - _lastPvUpdateTime; + if (deltaTime > 0) { // Avoid division by zero and ensure time has passed + int32_t deltaPV = (int32_t)_currentPV - (int32_t)_previousPV; // Use int32_t for intermediate calculation + // Calculate rate in Tenths of PV Unit per Minute + float ratePerMinute = (static_cast(deltaPV) * 60000.0f) / static_cast(deltaTime); + // Clamp to int16_t range + if (ratePerMinute > 32767.0f) ratePerMinute = 32767.0f; + if (ratePerMinute < -32768.0f) ratePerMinute = -32768.0f; + _pvSpLag = static_cast(ratePerMinute); + } // else deltaTime is 0, keep previous rate + } else { + _pvSpLag = 0; // No rate calculable yet + _hasPreviousPv = true; // Mark that we now have a previous value for the next iteration + } + _previousPV = _currentPV; + _lastPvUpdateTime = now; + } else { + // If PV is not valid, reset the rate calculation state + _pvSpLag = 0; + _hasPreviousPv = false; + } + + // --- Error Statistics --- + // Calculate and add error to statistics if PV and SP are valid + if (_pvValid && _spValid) { + float error = abs((int16_t)_currentPV - (int16_t)_currentSP); // Calculate absolute error + _errorStats.add(error); + } + + // --- Heating Interval Statistics and Wh Calculation --- + bool heatingNow = isHeating(); + // Note: 'now' should be defined earlier in the #ifdef block, e.g., millis_t now = millis(); + + // Track heating intervals for rate calculation + if (!_wasHeating && heatingNow) { // Heating just started + _heatOnStartTime = now; + } else if (_wasHeating && !heatingNow) { // Heating just stopped + millis_t duration = now - _heatOnStartTime; + if (duration > 0) { // Avoid adding zero or negative duration + _heatingIntervalStats.add(duration); + } + } + + // --- Longest Heating Duration in 60s Window --- + // Check if the 60s window needs to reset + if (now - _windowStartTime >= WINDOW_DURATION_MS) { + _windowStartTime = now; // Reset window start time + _maxHeatDurationInWindowSecs = 0; // Reset max duration for the new window + } + + if (heatingNow) { + if (_currentHeatStartTime == 0) { // Just started heating in this continuous block + _currentHeatStartTime = now; + } + // Calculate the duration of the *current* continuous heating period so far + millis_t currentDurationMs = now - _currentHeatStartTime; + uint16_t currentDurationSecs = (currentDurationMs + 500) / 1000; // Round to nearest second + + // Update the maximum duration within the current window if this is longer + if (currentDurationSecs > _maxHeatDurationInWindowSecs) { + _maxHeatDurationInWindowSecs = currentDurationSecs; + } + } else { + // Heating is off, reset the start time for the next continuous block + _currentHeatStartTime = 0; + } + + // Continuous Wh calculation (based on original logic) + if (heatingNow) { + // Only calculate Wh delta if _lastHeatingLoopTime is valid (meaning it was heating last loop too) + if (_lastHeatingLoopTime != 0) { + millis_t deltaMs = now - _lastHeatingLoopTime; + if (deltaMs > 0) { // Avoid division by zero or negative time + float deltaHours = static_cast(deltaMs) / 3600000.0f; + float deltaWh = static_cast(_consumption) * deltaHours; + _totalWh += deltaWh; + } + } + // Always update _lastHeatingLoopTime when heating is active + _lastHeatingLoopTime = now; + } else { + // Reset when heating stops, so Wh calculation restarts correctly next time heating begins + _lastHeatingLoopTime = 0; + } + + // Update state for next iteration *after* all calculations using _wasHeating + _wasHeating = heatingNow; // Store current state for next iteration +#endif + + // Example: Check staleness based on lastResponseTime (public member of RTU_Base) + // if (lastResponseTime > 0 && (millis() - lastResponseTime > (_readInterval * 2))) { + // Log.warningln(F("OmronE5[%d]: Data might be stale (last response %lu ms ago)."), slaveId, millis() - lastResponseTime); + // } + + return 0; // Return non-zero to indicate an error +} +short OmronE5::info() +{ + uint16_t pv, sp, statusL, statusH; + bool pvOk = getPV(pv); + bool spOk = getSP(sp); + bool statusLOk = getStatusLow(statusL); + bool statusHOk = getStatusHigh(statusH); + + Log.infoln(F("--- OmronE5[%d] Info ---"), slaveId); + Log.infoln(F(" State: %s"), getStateString()); + Log.infoln(F(" Last Response: %lu ms ago"), (lastResponseTime > 0) ? (millis() - lastResponseTime) : 0); + Log.infoln(F(" Error Count: %u"), errorCount); + + Log.infoln(F(" PV: %s (%d)"), pvOk ? "OK" : "Error/Missing", pvOk ? pv : 0); + Log.infoln(F(" SP: %s (%d)"), spOk ? "OK" : "Error/Missing", spOk ? sp : 0); + Log.infoln(F(" Status Low: %s (0x%04X)"), statusLOk ? "OK" : "Error/Missing", statusLOk ? statusL : 0); + Log.infoln(F(" Status High: %s (0x%04X)"), statusHOk ? "OK" : "Error/Missing", statusHOk ? statusH : 0); +#ifdef ENABLE_TRUTH_COLLECTOR + // Cast float values to integers for logging + int meanErrorInt = static_cast(getMeanError()); + Log.infoln(F(" Mean PV-SP Error (int): %d (Count: %u)"), meanErrorInt, _errorStats.count()); // Display mean error as integer +#endif +#ifdef ENABLE_COOLING + uint32_t coolMV; + if (getCoolingMVRaw(coolMV)) { + Log.infoln(F(" Cooling MV: %s (0x%04X)"), "OK", coolMV); + } else { + Log.infoln(F(" Cooling MV: %s"), "Error/Missing"); + } +#endif + + // Display decoded status flags + if (statusLOk && statusHOk) + { + Log.infoln(F(" Decoded Status:")); + Log.infoln(F(" Running: %s"), isRunning() ? "Yes" : "No"); + Log.infoln(F(" Heating: %s"), isHeating() ? "Yes" : "No"); + Log.infoln(F(" Cooling: %s"), isCooling() ? "Yes" : "No"); + Log.infoln(F(" Heatup: %s"), isHeatup() ? "Yes" : "No"); + Log.infoln(F(" AutoTuning: %s"), isAutoTuning() ? "Yes" : "No"); +#ifdef ENABLE_TRUTH_COLLECTOR + int heatRateInt = static_cast(getHeatRate()); + Log.infoln(F(" Avg Heat Rate (int): %d osc/min (Count: %u)"), heatRateInt, _heatingIntervalStats.count()); // Display heat rate as integer + + uint32_t totalWhInt = static_cast(getTotalWh()); + Log.infoln(F(" Total Consumption (int): %u Wh (%u kWh)"), totalWhInt, totalWhInt / 1000); // Display Wh and kWh as integers + + // --- Cost Calculation --- + float totalKWh = static_cast(totalWhInt) / 1000.0f; + float totalCostEuros = totalKWh * PRICE_PER_KWH_EUROS; + char costStr[10]; // Buffer for cost string + dtostrf(totalCostEuros, 4, 2, costStr); // width=4, precision=2 for Euros.Cents + Log.infoln(F(" Estimated Cost: %s EUR"), costStr); + Log.infoln(F(" Consumption: %d W"), getConsumption()); + Log.infoln(F(" PV SP Lag: %d (tenths/min)"), _pvSpLag); + Log.infoln(F(" Longest Heat (5 min window): %u s"), getLongestHeatDuration60s()); // Display new metric + //Log.infoln(F(" Cooling MV: %s (0x%04X)"), _coolingMvValid ? "OK" : "Error/Missing", _coolingMvValid ? _currentCoolingMVLow : 0); +#endif + } + else + { + Log.infoln(F(" Decoded Status: Unknown (missing status registers)")); + } + + Log.infoln(F(" Read Interval: %lu ms"), _readInterval); + Log.infoln(F("--- End OmronE5[%d] Info --- "), slaveId); + return 0; +} +bool OmronE5::getPV(uint16_t &value) const +{ + if (_pvValid) + { + value = _currentPV; + return true; + } + value = 0; + return false; +} +bool OmronE5::getSP(uint16_t &value) const +{ + if (_spValid) + { + value = _currentSP; + return true; + } + value = 0; + return false; +} +bool OmronE5::getStatusLow(uint16_t &value) const +{ + if (_statusValid) + { // Check combined validity + value = _currentStatusLow; + return true; + } + value = 0; + return false; +} +bool OmronE5::getStatusHigh(uint16_t &value) const +{ + if (_statusValid) + { // Check combined validity + value = _currentStatusHigh; + return true; + } + value = 0; + return false; +} +bool OmronE5::isRunning() const +{ + uint16_t statusH, statusL; + if (getStatusHigh(statusH) && getStatusLow(statusL)) + { + // Note: OR_E5_STATUS_BIT is defined in OmronE5Types.h + // RunStop bit (24) is 0 when running, 1 when stopped. + return !OR_E5_STATUS_BIT(statusH, statusL, OR_E5_S1_RunStop); + } + return false; // Cannot determine state if registers are missing +} +bool OmronE5::isHeating() const +{ + uint16_t statusH, statusL; + if (getStatusHigh(statusH) && getStatusLow(statusL)) + { + // Control_OutputOpenOutput bit (8) is 1 when heating output is ON. + return OR_E5_STATUS_BIT(statusH, statusL, OR_E5_S1_Control_OutputOpenOutput); + } + return false; +} +bool OmronE5::isCooling() const +{ + uint16_t statusH, statusL; + if (getStatusHigh(statusH) && getStatusLow(statusL)) + { + // Control_OutputCloseOutput bit (9) is 1 when cooling output is ON. + return OR_E5_STATUS_BIT(statusH, statusL, OR_E5_S1_Control_OutputCloseOutput); + } + return false; +} +bool OmronE5::isAutoTuning() const +{ + uint16_t statusH, statusL; + if (getStatusHigh(statusH) && getStatusLow(statusL)) + { + // ATExcecute bit (23) is 1 when AT is executing. + return OR_E5_STATUS_BIT(statusH, statusL, OR_E5_S1_ATExcecute); + } + return false; +} +bool OmronE5::setSP(uint16_t value) +{ + uint16_t spAddr = OR_E5_SWR_SP; + // Clamp the value to the maximum allowed SP + uint16_t clampedValue = value > OMRON_E5_SP_MAX ? OMRON_E5_SP_MAX : value; + if (clampedValue != value) { + Log.warningln(F("OmronE5[%d]: setSP requested value %d clamped to %d"), this->slaveId, value, clampedValue); + } + this->setOutputRegisterValue(spAddr, clampedValue); + return true; +} +bool OmronE5::run() +{ + uint16_t cmdAddr = OR_E5_CMD_ADDRESS::OR_E5_CMD_STOP_RUN; + Log.infoln(F("OmronE5[%d]: Setting RUN command (Addr=0x%04X, Val=0x%04X)"), slaveId, cmdAddr, 1); + this->setOutputRegisterValue(cmdAddr, 0); + return true; +} +bool OmronE5::stop() +{ + uint16_t cmdAddr = OR_E5_CMD_ADDRESS::OR_E5_CMD_STOP_RUN; + Log.infoln(F("OmronE5[%d]: Setting STOP command (Addr=0x%04X, Val=0x%04X)"), slaveId, cmdAddr, 0); + this->setOutputRegisterValue(cmdAddr, 1); + return true; +} +void OmronE5::onRegisterUpdate(uint16_t address, uint16_t newValue) +{ + // Define addresses based on offsets + const uint16_t pvAddress = OMRON_E5_READ_BLOCK_START_ADDR + 1; + const uint16_t statusHighAddress = OMRON_E5_READ_BLOCK_START_ADDR + 2; + const uint16_t statusLowAddress = OMRON_E5_READ_BLOCK_START_ADDR + 3; + const uint16_t spAddress = OMRON_E5_READ_BLOCK_START_ADDR + 5; + + // Store received value in local members and set validity flags + if (address == pvAddress) + { + // Log.infoln(F("OmronE5[%d]: Internal update for PV (Addr: %d) -> %d"), slaveId, address, newValue); + _currentPV = newValue; + _pvValid = true; + } + else if (address == spAddress) + { + _currentSP = newValue; + _spValid = true; + } + else if (address == statusLowAddress) + { + _currentStatusLow = newValue; + _statusValid = _statusValid || _currentStatusHigh != 0; // Mark valid only if both halves received at least once + } + else if (address == statusHighAddress) + { + _currentStatusHigh = newValue; + _statusValid = _statusValid || _currentStatusLow != 0; // Mark valid only if both halves received at least once + } +#ifdef ENABLE_COOLING + else if (address == 0x2005) // E_MV_MONITOR_COOL_REGISTER Low Word + { + // Log.infoln(F("OmronE5[%d]: Internal update for Cooling MV Low (Addr: 0x%04X) -> 0x%04X"), slaveId, address, newValue); + _currentCoolingMVLow = newValue; + // Validity requires both low and high words to be received at least once + _coolingMvValid = _coolingMvValid || (_currentCoolingMVHigh != 0); + } + else if (address == 0x2006) // E_MV_MONITOR_COOL_REGISTER High Word + { + // Log.infoln(F("OmronE5[%d]: Internal update for Cooling MV High (Addr: 0x%04X) -> 0x%04X"), slaveId, address, newValue); + _currentCoolingMVHigh = newValue; + // Validity requires both low and high words to be received at least once + _coolingMvValid = _coolingMvValid || (_currentCoolingMVLow != 0); + } +#endif + else + { + //Log.traceln(F("OmronE5[%d]: Internal update for other register (Addr: 0x%04X) -> %d"), slaveId, address, newValue); + } + // Call info() for debugging - it should now show correct values via getters + // info(); + RTU_Base::onRegisterUpdate(address, newValue); +} + +#ifdef ENABLE_TRUTH_COLLECTOR +float OmronE5::getMeanError() const { + return _errorStats.mean(); +} +float OmronE5::getHeatRate() const { + if (_heatingIntervalStats.count() == 0) { + return 0.0f; // No data yet + } + float avgIntervalMs = _heatingIntervalStats.average(); + if (isnan(avgIntervalMs) || avgIntervalMs <= 0.0f) { + return 0.0f; // Invalid average interval + } + // Convert average interval (ms/osc) to oscillations per minute + return (1.0f / avgIntervalMs) * 60000.0f; +} + +float OmronE5::getTotalWh() const { + return _totalWh; +} + +uint16_t OmronE5::getLongestHeatDuration60s() const { + // Return the tracked maximum duration in seconds for the current/last 60s window + return _maxHeatDurationInWindowSecs; +} +#endif // ENABLE_TRUTH_COLLECTOR + +uint32_t OmronE5::getConsumption() const { + // Return the pre-configured consumption value for this specific Omron instance + return _consumption; +} + +#ifdef ENABLE_COOLING +// --- Cooling Specific Getter Implementation --- +bool OmronE5::getCoolingMVRaw(uint32_t& value) const +{ + if (_coolingMvValid) + { + // Combine high and low words into a 32-bit value + value = (static_cast(_currentCoolingMVHigh) << 16) | _currentCoolingMVLow; + return true; + } + value = 0; + return false; +} +#endif + +bool OmronE5::isHeatup() const +{ + // Check if PV and SP are valid and SP is greater than PV + if (_pvValid && _spValid && _currentSP > _currentPV) + { + // Calculate the deadband value as a percentage of the current SP + float deadbandValue = static_cast(_currentSP) * (HEATUP_DEADBAND / 100.0f); + + // Check if the difference between SP and PV is greater than the calculated deadband + return static_cast(_currentSP - _currentPV) > deadbandValue; + } + return false; // Cannot determine state if PV or SP is missing or PV >= SP +} + +// --- End of OmronE5.cpp --- + +#endif // ENABLE_RS485 + +#ifdef ENABLE_TRUTH_COLLECTOR +// --- Helper to reset runtime statistics --- +void OmronE5::_resetRuntimeStats() { + Log.infoln(F("OmronE5[%d]: Resetting runtime statistics..."), this->slaveId); + _errorStats.clear(); + _heatingIntervalStats.clear(); + _totalWh = 0.0f; + _pvSpLag = 0; + _lastPvUpdateTime = 0; + _hasPreviousPv = false; + _lastHeatingLoopTime = 0; + _currentHeatStartTime = 0; + _windowStartTime = 0; + _maxHeatDurationInWindowSecs = 0; +} +#endif // ENABLE_TRUTH_COLLECTOR \ No newline at end of file diff --git a/src/components/OmronE5.h b/src/components/OmronE5.h new file mode 100644 index 00000000..3281e500 --- /dev/null +++ b/src/components/OmronE5.h @@ -0,0 +1,167 @@ +#ifndef OMRON_E5_H +#define OMRON_E5_H + +#include "config.h" + + +#ifdef ENABLE_RS485 + +#include +#include +#include "modbus/ModbusRTU.h" +#include "modbus/ModbusTypes.h" +#include "components/OmronE5Types.h" + +// #define ENABLE_COOLING +#define ENABLE_TRUTH_COLLECTOR + +#ifdef ENABLE_TRUTH_COLLECTOR +#include +#endif + +#define OMRON_E5_READ_BLOCK_START_ADDR 0x0000 +#define OMRON_E5_READ_BLOCK_REG_COUNT 6 +#define OMRON_TCP_REG_RANGE 16 +#define OMRON_E5_READ_BLOCK_INTERVAL 500 + +class OmronE5 : public RTU_Base +{ +public: + // Calculate the number of TCP blocks based on conditional compilation + #ifdef ENABLE_TRUTH_COLLECTOR + static constexpr int OMRON_TCP_BLOCK_COUNT = 16; + #else + #ifdef ENABLE_COOLING // Added check for cooling when truth collector is off + static constexpr int OMRON_TCP_BLOCK_COUNT = 8; // Basic + CMDs + Heatup + CoolingMV + #else + static constexpr int OMRON_TCP_BLOCK_COUNT = 7; // Basic + CMDs + Heatup + #endif + #endif + + // Constructor + OmronE5(uint8_t slaveId, millis_t readInterval = 5000); + virtual ~OmronE5() = default; + + // --- Component Interface --- + virtual short setup() override; + virtual short loop() override; + virtual short info() override; + + // --- Modbus Register Update Notification --- + virtual void onRegisterUpdate(uint16_t address, uint16_t newValue) override; // Override base method + + // --- Getters for Specific Values --- + // These read from the RTU_Device's internal register map based on offsets + // within the 0x0000 block. + bool getPV(uint16_t& value) const; + bool getSP(uint16_t& value) const; + bool getStatusLow(uint16_t& value) const; // New getter for Status Low Word + bool getStatusHigh(uint16_t& value) const; // New getter for Status High Word + + // --- Status Flag Checkers (based on OmronPID ref) --- + bool isRunning() const; + bool isHeating() const; + bool isCooling() const; + bool isAutoTuning() const; +#ifdef ENABLE_TRUTH_COLLECTOR + float getMeanError() const; + float getHeatRate() const; + float getTotalWh() const; + uint16_t getLongestHeatDuration60s() const; // Getter returns seconds +#endif + bool isHeatup() const; // Added heatup state check + + // --- Setters (Optional - Implement if needed) --- + uint16_t getSpCmdAddress(uint8_t slaveId); // Declaration added + bool setSP(uint16_t value); + bool run(); // Use 'run' to match OmronPID::runAll terminology + bool stop(); + uint32_t getConsumption() const; // Added based on AmperageBudgetManager design + +#ifdef ENABLE_COOLING + // --- Cooling Specific Getters --- + // Requires ENABLE_COOLING define in OmronE5.cpp and manual Omron device setup for cooling. + /** + * @brief Gets the raw 32-bit value of the Cooling Manipulated Variable (MV) monitor. + * Requires reading registers 0x2005 (Low) and 0x2006 (High). + * @param value Reference to store the combined 32-bit value. + * @return True if the value is valid (has been successfully read), false otherwise. + */ + bool getCoolingMVRaw(uint32_t& value) const; +#endif + + // --- Modbus Block Definitions --- + virtual ModbusBlockView* mb_tcp_blocks() const override; + virtual short mb_tcp_read(MB_Registers * reg) override; + virtual short mb_tcp_write(MB_Registers * reg, short value) override; + + // --- Modbus TCP Mapping Overrides --- + /** + * @brief Gets the base Modbus TCP address allocated for this RTU device instance. + * @return The base TCP address for this device instance. + */ + uint16_t mb_tcp_base_address() const override; + + /** + * @brief Calculates the Modbus TCP offset corresponding to a given RTU address update. + * @param rtuAddress The RTU register address that was updated. + * @return The corresponding TCP offset (relative to mb_tcp_base_address), or 0 if no direct mapping exists for broadcast. + */ + uint16_t mb_tcp_offset_for_rtu_address(uint16_t rtuAddress) const override; + +#ifdef ENABLE_TRUTH_COLLECTOR + /** + * @brief Resets all runtime statistics and timestamps used by the truth collector. + * Triggered by writing 2 to the CMD_EXECUTE register. + */ + void _resetRuntimeStats(); +#endif + +private: + millis_t _readInterval; + + // --- Local State Storage --- + uint16_t _currentPV = 0; + uint16_t _currentSP = 0; + uint16_t _currentStatusLow = 0; + uint16_t _currentStatusHigh = 0; + bool _pvValid = false; + bool _spValid = false; + bool _statusValid = false; // Combined flag for low/high status pair + uint32_t _consumption = 2700; // Added consumption member (Watts) with default value + +#ifdef ENABLE_TRUTH_COLLECTOR + Statistic _errorStats; + Statistic _heatingIntervalStats; + bool _wasHeating = false; + millis_t _heatOnStartTime = 0; + float _totalWh = 0.0f; + + // PV Rate of Change Calculation (PV SP Lag) + uint16_t _previousPV = 0; + millis_t _lastPvUpdateTime = 0; + int16_t _pvSpLag = 0; // Tenths of PV Unit per Minute (PV SP Lag) + bool _hasPreviousPv = false; + + millis_t _lastHeatingLoopTime = 0; // Timestamp for continuous Wh calculation + + // Longest Heating Duration Tracking (60s window) + millis_t _currentHeatStartTime = 0; // Start time of the current continuous heating period + millis_t _windowStartTime = 0; // Start time of the 60-second tracking window + uint16_t _maxHeatDurationInWindowSecs = 0; // Longest duration (seconds) found in the current window +#endif + +#ifdef ENABLE_COOLING + // --- Cooling State (Requires ENABLE_COOLING in .cpp & manual device setup) --- + uint16_t _currentCoolingMVLow = 0; + uint16_t _currentCoolingMVHigh = 0; + bool _coolingMvValid = false; +#endif + + // NOTE: Size updated to match OMRON_TCP_BLOCK_COUNT + MB_Registers _modbusBlocks[OMRON_TCP_BLOCK_COUNT]; + ModbusBlockView _modbusBlockView; +}; + +#endif // ENABLE_RS485 +#endif // OMRON_E5_H \ No newline at end of file diff --git a/src/components/OmronE5Types.h b/src/components/OmronE5Types.h new file mode 100644 index 00000000..3ac23103 --- /dev/null +++ b/src/components/OmronE5Types.h @@ -0,0 +1,361 @@ +#ifndef OMRON_E5_TYPES_H +#define OMRON_E5_TYPES_H + +// Omron EJ5 Modbus Registers & Coils + +#define OR_BIT(A) (A >> 1) +#define OR_WORD(A) (A << 4) + +#define OR_E5_STATUS_BIT(H, L, B) (B <= 16 ? (L & (1 << 8)) : (OR_WORD(H) & (1 << (OR_BIT(B))))) +#define OR_E5_CMD(CMD, VALUE) (CMD | VALUE) + +// Status Bit -1 , see h175_e5_c_communications_manual_en.pdf::3-24 +enum OR_E5_STATUS_1 +{ + // Lower Word + + OR_E5_S1_Heater_OverCurrent = 0, + OR_E5_S1_Heater_CurrentHold = 1, + OR_E5_S1_AD_ConverterError = 2, + OR_E5_S1_HS_Alarm = 3, + OR_E5_S1_RSP_InputError = 4, + OR_E5_S1_InputError = 6, + OR_E5_S1_PotentiometerInputError = 7, + OR_E5_S1_Control_OutputOpenOutput = 8, + OR_E5_S1_Control_OutputCloseOutput = 9, + OR_E5_S1_HBAlarmCT1 = 10, + OR_E5_S1_HBAlarmCT2 = 11, + OR_E5_S1_Alarm1 = 12, + OR_E5_S1_Alarm2 = 13, + OR_E5_S1_Alarm3 = 14, + OR_E5_S1_ProgramEndOutput = 15, + + // Upper Word + + OR_E5_S1_EventInput1 = 16, + OR_E5_S1_EventInput2 = 17, + OR_E5_S1_EventInput3 = 18, + OR_E5_S1_EventInput4 = 19, + OR_E5_S1_WriteMode = 20, + OR_E5_S1_NonVolatileMemory = 21, + OR_E5_S1_SetupArea = 22, + OR_E5_S1_ATExcecute = 23, + OR_E5_S1_RunStop = 24, + OR_E5_S1_ComWrite = 25, + OR_E5_S1_AutoManualSwitch = 26, + OR_E5_S1_ProgramStart = 27, + OR_E5_S1_HeaterOverCurrentCT2 = 28, + OR_E5_S1_HeaterCurrentHoldCT2 = 29, + OR_E5_S1_HSAlarmCT2 = 31 +}; + +// Status Bit - 2 , see h175_e5_c_communications_manual_en.pdf::3-25 + +enum OR_E5_STATUS_2 +{ + // Lower Word + + OR_E5_S2_WorkBit1 = 0, + OR_E5_S2_WorkBit2 = 1, + OR_E5_S2_WorkBit3 = 2, + OR_E5_S2_WorkBit4 = 3, + OR_E5_S2_WorkBit5 = 4, + OR_E5_S2_WorkBit6 = 5, + OR_E5_S2_WorkBit7 = 6, + OR_E5_S2_WorkBit8 = 7, + + // Upper Word + + OR_E5_S2_EventInput5 = 16, + OR_E5_S2_EventInput6 = 17, + OR_E5_S2_Inverse = 20, + OR_E5_S2_SPRamp = 21, + OR_E5_S2_SPMode = 27, + OR_E5_S2_Alarm4 = 28 +}; + +// Variable Area - Settings Range (0x06s) - 2 byte mode, +// see h175_e5_c_communications_manual_en.pdf::5-1 + +enum OR_E5_SWR +{ + //Temperature: Use the specified range for each sensor. + // Analog: Scaling lower limit − 5% FS to Scaling upper limit + 5% FS + OR_E5_SWR_PV = 0x2000, + + // Refer to 5-2 Status for details (see @OR_E5_STATUS_1 and @OR_E5_STATUS_2) + OR_E5_SWR_STATUS = 0x2001, + + // Internal Set Point(see appendix *1) - SP lower limit to SP upper limit + OR_E5_SWR_ISP = 0x2002, + + // Heater Current 1 Value Monitor, 0x00000000 to 0x00000226 (0.0 to 55.0) + OR_E5_SWR_HeaterCurrentValue1_Monitor = 0x2003, + + // MV Monitor (Heating) + // Standard: 0xFFFFFFCE to 0x0000041A (−5.0 to 105.0) + // Heating and cooling: 0x00000000 to 0x0000041A (0.0 to 105.0) + OR_E5_SWR_MVMonitorHeating = 0x2004, + + // MV Monitor (Cooling) + // 0x00000000 to 0x0000041A (0.0 to 105.0) + OR_E5_SWR_MVMonitorCooling = 0x2005, + + // Set Point - SP lower limit to SP upper limit + OR_E5_SWR_SP_LIMIT = 0x2103, + + // Alarm Value 1 + // 0xFFFFF831 to 0x0000270F (−1999 to 9999) + OR_E5_SWR_ALARM_1 = 0x2104, + + // Alarm Value - Upper Limit 1 + // 0xFFFFF831 to 0x0000270F (−1999 to 9999) + OR_E5_SWR_ALARM_1_UL = 0x2105, + + // Alarm Value - Lower Limit 1 + // 0xFFFFF831 to 0x0000270F (−1999 to 9999) + OR_E5_SWR_ALARM_1_LL = 0x2106, + + // Alarm Value 2 + // 0xFFFFF831 to 0x0000270F (−1999 to 9999) + OR_E5_SWR_ALARM_2 = 0x2107, + + // Alarm Value - Upper Limit 1 + // 0xFFFFF831 to 0x0000270F (−1999 to 9999) + OR_E5_SWR_ALARM_2_UL = 0x2108, + + // Alarm Value - Lower Limit 1 + // 0xFFFFF831 to 0x0000270F (−1999 to 9999) + OR_E5_SWR_ALARM_2_LL = 0x2109, + + //Temperature: Use the specified range for each sensor. + // Analog: Scaling lower limit − 5% FS to Scaling upper limit + 5% FS + OR_E5_SWR_PV2 = 0x2402, + + // Internal Set Point(see appendix *1) - SP lower limit to SP upper limit + OR_E5_SWR_ISP2 = 0x2403, + + // Multi SP No. Monitor, 0x00000000 to 0x00000007 (0 to 7) + OR_E5_SWR_MSMON = 0x2404, + + // Status, + // - Not displayed on the Controller display. + // - In 2-byte mode, the rightmost 16 bits are read. + OR_E5_SWR_STATUSEX = 0x2406, + + // Status, + // - Not displayed on the Controller display. + // - In 2-byte mode, the leftmost 16 bits are read. + OR_E5_SWR_STATUSEXL = 0x2407, + + // Status, + // - Not displayed on the Controller display. + // - In 2-byte mode, the rightmost 16 bits are read. + OR_E5_SWR_STATUSEXR = 0x2408, + + // Decimal Point Monitor, + // 0x00000000 to 0x00000003 (0 to 3) + OR_E5_SWR_DECMON = 0x2410, + + // Set Point () + // SP lower limit to SP upper limit + OR_E5_SWR_SP = 0x2601, + + // Remote Set Point Monitor + // - Remote SP lower limit −10% FS to Remote SP upper limit +10% FS + OR_E5_SWR_SP_EX_MON = 0x2602, + + // Heater Current 1 Value Monitor, 0x00000000 to 0x00000226 (0.0 to 55.0) + OR_E5_SWR_HeaterCurrentValue1_Monitor2 = 0x2604, + + // Valve Opening Monitor, 0xFFFFFF9C to 0x0000044C (−10.0 to 110.0) + OR_E5_SWR_VALVE_OPENING_MON = 0x2607, + + // Proportional Band (Cooling), 0x00000001 to 0x0000270F (0.1 to 999.9) + OR_E5_SWR_PRO_BAND = 0x2701, + + // Integral Time (Cooling) 0x00000000 to 0x0000270F + // (0 to 9999: Integral/derivative time unit is 1 s.) + // (0.0 to 999.9: Integral/derivative time unit is 0.1 s.) + OR_E5_SWR_IT_COOLING = 0x2702, + + // Derivative Time (Cooling) 0x00000000 to 0x0000270F + // (0 to 9999: Integral/derivative time unit is 1 s.) + // (0.0 to 999.9: Integral/derivative time unit is 0.1 s.) + OR_E5_SWR_D_COOLING = 0x2703, + + // Dead Band 0xFFFFF831 to 0x0000270F + // (−199.9 to 999.9 for temperature input) + // (−19.99 to 99.99 for analog input) + OR_E5_SWR_DEADBAND = 0x2704, + + // Manual Reset Value, + // 0x00000000 to 0x000003E8 (0.0 to 100.0) + OR_E5_SWR_MANUAL_RESET_VALUE = 0x2705, + + // Hysteresis (Heating) + // 0x00000001 to 0x0000270F + // (0.1 to 999.9 for temperature input) + // (0.01 to 99.99 for analog input) + OR_E5_SWR_HYSTERESIS = 0x2706, + + // Hysteresis (Cooling) + // 0x00000001 to 0x0000270F + // (0.1 to 999.9 for temperature input) + // (0.01 to 99.99 for analog input) + OR_E5_SWR_HYSTERESIS_COOLING = 0x2707, + + // Control Period (Heating) + // 0xFFFFFFFE (−2): 0.1 s + // 0xFFFFFFFF (−1): 0.2 s + // 0x00000000 (0): 0.5 s + // 0x00000001 to 0x00000063 (1 to 99) + OR_E5_SWR_CONTROL_PERIOD_HEATING = 0x2708, + + // Control Period (Cooling) + // 0xFFFFFFFE (−2): 0.1 s + // 0xFFFFFFFF (−1): 0.2 s + // 0x00000000 (0): 0.5 s + // 0x00000001 to 0x00000063 (1 to 99) + OR_E5_SWR_CONTROL_PERIOD_COOLING = 0x2709, + + // Position Proportional Dead Band + // 0x00000001 to 0x00000064 (0.1 to 10.0) + OR_E5_SWR_POSITION_PROPORTIONAL_DEAD_BAND = 0x270A, + + // Open/Close Hysteresis + // 0x00000001 to 0x000000C8 (0.1 to 20.0) + OR_E5_SWR_OPEN_CLOSE_HYSTERESIS = 0x270B, + + // SP Ramp Time Unit 0x00000000 (0): EU/second + // 0x00000001 (1): EU/minute + // 0x00000002 (2): EU/hour + OR_E5_SWR_SP_RAMP_UNIT = 0x270C, + + // SP Ramp Set Value 0x00000000 (0): OFF + // 0x00000001 to 0x0000270F (1 to 9999) + OR_E5_SWR_SP_RAMP_SET_VALUE = 0x270D, + + // SP Ramp Fall Value + // 0xFFFFFFFF (−1): Same (Same as SP Ramp Set Value.) + // 0x00000000 (0): OFF + // 0x00000001 to 0x0000270F (1 to 9999) + OR_E5_SWR_SP_FALL_VALUE = 0x270E, + + // MV at Stop Standard Models + // Standard control: + // 0xFFFFFFCE to 0x0000041A (−5.0 to 105.0) + // Heating and cooling control: + // 0xFFFFFBE6 to 0x0000041A (−105.0 to 105.0) + // Position-proportional Models + // Close position-proportional control with the Direct Setting of + // Position Proportional MV parameter set to ON: + // 0xFFFFFFCE to 0x0000041A (−5.0 to 105.0) + // Floating position-proportional control or the Direct Setting of + // Position Proportional MV parameter set to OFF: + // 0xFFFFFFFF to 0x00000001 (−1 to 1) + OR_E5_SWR_MV_PV_ERROR = 0x2711, + + // MV Change Rate Limit + // 0x00000000 to 0x000003E8 (0.0 to 100.0) + OR_E5_SWR_CHANGE_RATE_LIMIT = 0x2713, + + // PV Input Slope Coefficient + // 0x00000001 to 0x0000270F (0.001 to 9.999) + OR_E5_SWR_PV_INPUT_SLOPE_COEFFICIENT = 0x2718, + + // Heater Burnout Detection 1 + // 0x00000000 to 0x000001F4 (0.0 to 50.0) + OR_E5_SWR_HEATER_BURNOUT_DETECTION_1 = 0x271B, + + // Leakage Current 1 Monitor + // 0x00000000 to 0x00000226 (0.0 to 55.0) + OR_E5_SWR_LEAKAGE_CURRENT_MONITOR_1 = 0x271C, + + // HS Alarm 1 + // 0x00000000 to 0x000001F4 (0.0 to 50.0) + OR_E5_SWR_HS_ALARM_1 = 0x271D, + + // Process Value Input Shift + // 0xFFFFF831 to 0x0000270F (−1999 to 9999) + OR_E5_SWR_PROCESS_VALUE_INPUT_SHIFT = 0x2723, + + // Heater Burnout Detection 2 + // 0x00000000 to 0x000001F4 (0.0 to 50.0) + OR_E5_SWR_HEATER_BURNOUT_DETECTION_2 = 0x2725, + + // Leakage Current 2 Monitor + // 0x00000000 to 0x00000226 (0.0 to 55.0) + OR_E5_SWR_LEAKAGE_CURRENT_MONITOR_2 = 0x2726, + + // HS Alarm 12 + // 0x00000000 to 0x000001F4 (0.0 to 50.0) + OR_E5_SWR_HS_ALARM_2 = 0x2727, + + // Soak Time Remain (how lovely) + // 0x00000000 to 0x0000270F (0 to 9999) + OR_E5_SWR_SOAK_REMAIN = 0x2728, + + // Soak Time + // 0x00000001 to 0x0000270F (1 to 9999) + OR_E5_SWR_SOAK_TIME = 0x2729, + + // Wait Band 0x00000000 (0): OFF + // 0x00000001 to 0x0000270F + // (0.1 to 999.9 for Temperature input) + // (0.01 to 99.99 for Analog input) + OR_E5_SWR_WAIT_BAND = 0x272A, + + // Remote SP Input Shift + // 0xFFFFF831 to 0x0000270F (−1999 to 9999) + OR_E5_SWR_REMOTE_SP_SHIFT = 0x272B, + + // Remote SP input Slope Coefficient + // 0x00000001 to 0x0 + OR_E5_SWR_REMOTE_SP_SLOPE_COEFFICIENT = 0x272C, + + // Input Digital Filter 0x00000000 to 0x0000270F (0.0 to 999.9) + OR_E5_SWR_DIGITAL_FILTER = 0x2800 + + // Notes : + // *1 Not displayed on the Controller display +}; + +enum OR_E5_CMD_ADDRESS +{ + OR_E5_CMD_COM_WRITE = 0, + OR_E5_CMD_STOP_RUN = 1, + OR_E5_CMD_AT = 0x200 +}; + +enum OR_E5_CMD +{ + OR_E5_STOP = OR_E5_CMD(OR_E5_CMD_ADDRESS::OR_E5_CMD_STOP_RUN, 1), + OR_E5_RUN = OR_E5_CMD(OR_E5_CMD_ADDRESS::OR_E5_CMD_STOP_RUN, 0), + OR_E5_AT_CANCEL = OR_E5_CMD(OR_E5_CMD_ADDRESS::OR_E5_CMD_AT, 0), + OR_E5_AT_EXCECUTE = OR_E5_CMD(OR_E5_CMD_ADDRESS::OR_E5_CMD_AT, 1) +}; + + +enum OR_E5_ERROR +{ + VARIABLE_ADDRESS_ERROR = 0x2, + VARIABLE_RANGE_ERROR = 0x3, + VARIABLE_OPERATION_ERROR = 0x4 +}; + +enum OR_E5_RESPONSE_CODE +{ + OR_READ_ERROR = 0x83, + OR_RESPONSE_OK = 0x10, + OR_OPERATION_ERROR = 0x90, + OR_COMMAND_ERROR = 0x86 +}; + +#define OR_E_MSG_INVALID_ADDRESS "Invalid Variable Address" +#define OR_E_MSG_INVALID_RANGE "Invalid Variable Range" +#define OR_E_MSG_OPERATION_ERROR "OPERATION ERROR" + + + +#endif \ No newline at end of file diff --git a/src/components/OmronE5_Ex.h b/src/components/OmronE5_Ex.h new file mode 100644 index 00000000..5ff0b3d4 --- /dev/null +++ b/src/components/OmronE5_Ex.h @@ -0,0 +1,260 @@ +#ifndef OMRONE5_EX_H +#define OMRONE5_EX_H + +enum class ModbusAddresses { + OR_E5_SWR_PV_TEMPERATURE = 0x2000, // PV Temperature: Use the specified range for each sensor. + OR_E5_SWR_STATUS = 0x2001, // Status: Refer to 5-2 Status for details. + OR_E5_SWR_INTERNAL_SET_POINT = 0x2002, // Internal Set Point: SP lower limit to SP upper limit. + OR_E5_SWR_HEATER_CURRENT_1_VALUE = 0x2003, // Heater Current 1 Value: Monitor current range 0.0 to 55.0. + OR_E5_SWR_MV_MONITOR_HEATING = 0x2004, // MV Monitor (Heating): Range from −5.0 to 105.0. + OR_E5_SWR_MV_MONITOR_COOLING = 0x2005, // MV Monitor (Cooling): Range from 0.0 to 105.0. + OR_E5_SWR_SET_POINT = 0x2103, // Set Point: SP lower limit to SP upper limit. + OR_E5_SWR_ALARM_VALUE_1 = 0x2104, // Alarm Value 1: Range from −1999 to 9999. + OR_E5_SWR_ALARM_VALUE_UPPER_LIMIT_1 = 0x2105, // Alarm Value Upper Limit 1: Range from −1999 to 9999. + OR_E5_SWR_ALARM_VALUE_LOWER_LIMIT_1 = 0x2106, // Alarm Value Lower Limit 1: Range from −1999 to 9999. + OR_E5_SWR_ALARM_VALUE_2 = 0x2107, // Alarm Value 2: Range from −1999 to 9999. + OR_E5_SWR_ALARM_VALUE_UPPER_LIMIT_2 = 0x2108, // Alarm Value Upper Limit 2: Range from −1999 to 9999. + OR_E5_SWR_ALARM_VALUE_LOWER_LIMIT_2 = 0x2109, // Alarm Value Lower Limit 2: Range from −1999 to 9999. + OR_E5_SWR_PV_TEMPERATURE_2 = 0x2402, // PV Temperature 2: Use the specified range for each sensor. + OR_E5_SWR_INTERNAL_SET_POINT_2 = 0x2403, // Internal Set Point 2: SP lower limit to SP upper limit. + OR_E5_SWR_MULTI_SP_NO_MONITOR = 0x2404, // Multi-SP No. Monitor: Range from 0 to 7. + OR_E5_SWR_STATUS_2 = 0x2406, // Status 2: Refer to 5-2 Status for details. + OR_E5_SWR_STATUS_3 = 0x2407, // Status 3: Refer to 5-2 Status for details. + OR_E5_SWR_STATUS_4 = 0x2408, // Status 4: Refer to 5-2 Status for details. + OR_E5_SWR_STATUS_5 = 0x2409, // Status 5: Refer to 5-2 Status for details. + OR_E5_SWR_DECIMAL_POINT_MONITOR = 0x2410, // Decimal Point Monitor: Range from 0 to 3. + // Add more entries as needed. + + + OR_E5_SWR_OPERATION_ADJUSTMENT_PROTECT = 0x2500, // Operation/Adjustment Protect: Control restrictions. + OR_E5_SWR_INITIAL_SETTING_COMMUNICATIONS_PROTECT = 0x2501, // Initial Setting/Communications Protect: Control restrictions. + OR_E5_SWR_SETTING_CHANGE_PROTECT = 0x2502, // Setting Change Protect: Controls change restrictions. + OR_E5_SWR_PF_KEY_PROTECT = 0x2503, // PF Key Protect: Protection settings for PF key. + OR_E5_SWR_MOVE_TO_PROTECT_LEVEL = 0x2504, // Move to Protect Level: Adjustment level setting range −1999 to 9999. + OR_E5_SWR_PASSWORD_TO_MOVE_TO_PROTECT_LEVEL = 0x2505, // Password to Move to Protect Level: Only settable. + OR_E5_SWR_PARAMETER_MASK_ENABLE = 0x2506, // Parameter Mask Enable: ON/OFF for parameter masking. + OR_E5_SWR_CHANGED_PARAMETERS_ONLY = 0x2507, // Changed Parameters Only: ON/OFF for display of changed parameters. + OR_E5_SWR_MANUAL_MV = 0x2600, // Manual MV: Manual operation and MV settings. + OR_E5_SWR_SET_POINT_2 = 0x2601, // Set Point: SP lower limit to SP upper limit. + OR_E5_SWR_REMOTE_SP_MONITOR = 0x2602, // Remote SP Monitor: Remote set point monitoring settings. + OR_E5_SWR_HEATER_CURRENT_1_VALUE_2 = 0x2604, // Heater Current 1 Value: Monitoring settings for heater current. + OR_E5_SWR_MV_MONITOR_HEATING_2 = 0x2605, // MV Monitor (Heating): Monitoring settings for MV in heating. + OR_E5_SWR_MV_MONITOR_COOLING_2 = 0x2606, // MV Monitor (Cooling): Monitoring settings for MV in cooling. + OR_E5_SWR_VALVE_OPENING_MONITOR = 0x2607, // Valve Opening Monitor: Valve position monitoring. + // Further entries truncated for brevity. Continue as necessary. + // ------------ Continuation from previous response... + OR_E5_SWR_PROPORTIONAL_BAND_COOLING = 0x2701, // Proportional Band (Cooling): Adjustment range 0.1 to 999.9. + OR_E5_SWR_INTEGRAL_TIME_COOLING = 0x2702, // Integral Time (Cooling): Time settings for cooling control. + OR_E5_SWR_DERIVATIVE_TIME_COOLING = 0x2703, // Derivative Time (Cooling): Time settings for derivative cooling. + OR_E5_SWR_DEAD_BAND = 0x2704, // Dead Band: Set range for dead band adjustments. + OR_E5_SWR_MANUAL_RESET_VALUE = 0x2705, // Manual Reset Value: Adjustment settings for manual reset. + OR_E5_SWR_HYSTERESIS_HEATING = 0x2706, // Hysteresis (Heating): Hysteresis settings for heating control. + OR_E5_SWR_HYSTERESIS_COOLING = 0x2707, // Hysteresis (Cooling): Hysteresis settings for cooling control. + OR_E5_SWR_CONTROL_PERIOD_HEATING = 0x2708, // Control Period (Heating): Timing settings for heating control. + OR_E5_SWR_CONTROL_PERIOD_COOLING = 0x2709, // Control Period (Cooling): Timing settings for cooling control. + OR_E5_SWR_POSITION_PROPORTIONAL_DEAD_BAND = 0x270A, // Position Proportional Dead Band: Settings for position proportional control. + OR_E5_SWR_OPEN_CLOSE_HYSTERESIS = 0x270B, // Open/Close Hysteresis: Settings for hysteresis in open/close control. + OR_E5_SWR_SP_RAMP_TIME_UNIT = 0x270C, // SP Ramp Time Unit: Units for SP ramp time settings. + OR_E5_SWR_SP_RAMP_SET_VALUE = 0x270D, // SP Ramp Set Value: Ramp settings for set points. + OR_E5_SWR_SP_RAMP_FALL_VALUE = 0x270E, // SP Ramp Fall Value: Ramp fall settings. + OR_E5_SWR_MV_AT_STOP = 0x270F, // MV at Stop: MV settings when the system is stopped. + OR_E5_SWR_MV_AT_PV_ERROR = 0x2711, // MV at PV Error: MV settings during a PV error. + OR_E5_SWR_MV_CHANGE_RATE_LIMIT = 0x2713, // MV Change Rate Limit: Limit settings for MV change rate. + OR_E5_SWR_PV_INPUT_SLOPE_COEFFICIENT = 0x2718, // PV Input Slope Coefficient: Slope settings for PV input. + OR_E5_SWR_HEATER_CURRENT_1_VALUE_3 = 0x271A, // Heater Current 1 Value: Current monitoring settings. + OR_E5_SWR_HEATER_BURNOUT_DETECTION_1 = 0x271B, // Heater Burnout Detection 1: Settings for detecting heater burnout. + OR_E5_SWR_LEAKAGE_CURRENT_1_MONITOR = 0x271C, // Leakage Current 1 Monitor: Monitoring leakage currents. + OR_E5_SWR_HS_ALARM_1 = 0x271D, // HS Alarm 1: High-speed alarm settings for channel 1. + OR_E5_SWR_PROCESS_VALUE_INPUT_SHIFT = 0x2723, // Process Value Input Shift: Shift adjustments for process value input. + OR_E5_SWR_HEATER_CURRENT_2_VALUE = 0x2724, // Heater Current 2 Value: Monitoring settings for heater current. + OR_E5_SWR_HEATER_BURNOUT_DETECTION_2 = 0x2725, // Heater Burnout Detection 2: Burnout detection settings. + OR_E5_SWR_LEAKAGE_CURRENT_2_MONITOR = 0x2726, // Leakage Current 2 Monitor: Monitoring settings for leakage current. + OR_E5_SWR_HS_ALARM_2 = 0x2727, // HS Alarm 2: High-speed alarm settings for channel 2. + OR_E5_SWR_SOAK_TIME_REMAIN = 0x2728, // Soak Time Remain: Monitoring remaining soak time. + OR_E5_SWR_SOAK_TIME = 0x2729, // Soak Time: Settings for soak time duration. + + + // --- Continuation from previous entries... + OR_E5_SWR_WAIT_BAND = 0x272A, // Wait Band: Wait band settings for various inputs. + OR_E5_SWR_REMOTE_SP_INPUT_SHIFT = 0x272B, // Remote SP Input Shift: Settings for shifting the remote SP input. + OR_E5_SWR_REMOTE_SP_INPUT_SLOPE_COEFFICIENT = 0x272C, // Remote SP Input Slope Coefficient: Slope settings for remote SP input. + OR_E5_SWR_INPUT_DIGITAL_FILTER = 0x2800, // Input Digital Filter: Filter settings for input signals. + OR_E5_SWR_MOVING_AVERAGE_COUNT = 0x2804, // Moving Average Count: Settings for the moving average count. + OR_E5_SWR_EXTRACTION_OF_SQUARE_ROOT_LOW_CUT_POINT = 0x2808, // Extraction of Square Root Low-cut Point: Settings for square root extraction low-cut. + OR_E5_SWR_SP_0 = 0x2900, // SP 0: Set point 0 for various control operations. + OR_E5_SWR_ALARM_VALUE_1_2 = 0x2902, // Alarm Value 1: Alarm settings for the first channel. + OR_E5_SWR_ALARM_VALUE_UPPER_LIMIT_1_2 = 0x2903, // Alarm Value Upper Limit 1: Upper limit for the first alarm channel. + OR_E5_SWR_ALARM_VALUE_LOWER_LIMIT_1_2 = 0x2904, // Alarm Value Lower Limit 1: Lower limit for the first alarm channel. + OR_E5_SWR_ALARM_VALUE_2_2 = 0x2905, // Alarm Value 2: Alarm settings for the second channel. + OR_E5_SWR_ALARM_VALUE_UPPER_LIMIT_2_2 = 0x2906, // Alarm Value Upper Limit 2: Upper limit for the second alarm channel. + OR_E5_SWR_ALARM_VALUE_LOWER_LIMIT_2_2 = 0x2907, // Alarm Value Lower Limit 2: Lower limit for the second alarm channel. + OR_E5_SWR_ALARM_VALUE_3 = 0x2908, // Alarm Value 3: Alarm settings for the third channel. + OR_E5_SWR_ALARM_VALUE_UPPER_LIMIT_3 = 0x2909, // Alarm Value Upper Limit 3: Upper limit for the third alarm channel. + OR_E5_SWR_ALARM_VALUE_LOWER_LIMIT_3 = 0x290A, // Alarm Value Lower Limit 3: Lower limit for the third alarm channel. + OR_E5_SWR_ALARM_VALUE_4 = 0x290B, // Alarm Value 4: Alarm settings for the fourth channel. + OR_E5_SWR_ALARM_VALUE_UPPER_LIMIT_4 = 0x290C, // Alarm Value Upper Limit 4: Upper limit for the fourth alarm channel. + OR_E5_SWR_ALARM_VALUE_LOWER_LIMIT_4 = 0x290D, // Alarm Value Lower Limit 4: Lower limit for the fourth alarm channel. + OR_E5_SWR_SP_1 = 0x290E, // SP 1: Set point 1 for various control operations. + OR_E5_SWR_SP_2 = 0x291C, // SP 2: Set point 2 for various control operations. + OR_E5_SWR_SP_3 = 0x292A, // SP 3: Set point 3 for various control operations. + OR_E5_SWR_SP_4 = 0x2938, // SP 4: Set point 4 for various control operations. + OR_E5_SWR_SP_5 = 0x2946, // SP 5: Set point 5 for various control operations. + OR_E5_SWR_SP_6 = 0x2954, // SP 6: Set point 6 for various control operations. + OR_E5_SWR_SP_7 = 0x2962, // SP 7: Set point 7 for various control operations. + OR_E5_SWR_PROPORTIONAL_BAND = 0x2A00, // Proportional Band: Proportional band settings for PID control. + OR_E5_SWR_INTEGRAL_TIME = 0x2A01, // Integral Time: Integral time settings for PID control. + OR_E5_SWR_DERIVATIVE_TIME = 0x2A02, // Derivative Time: Derivative time settings for PID control. + OR_E5_SWR_MV_UPPER_LIMIT = 0x2A05, // MV Upper Limit: Upper limit settings for MV. + OR_E5_SWR_MV_LOWER_LIMIT = 0x2A06, // MV Lower Limit: Lower limit settings for MV. + OR_E5_SWR_INPUT_TYPE = 0x2C00, // Input Type: Settings for the type of input sensor. + OR_E5_SWR_TEMPERATURE_UNIT = 0x2C01, // Temperature Unit: Settings for temperature unit display (°C/°F). + OR_E5_SWR_SCALING_LOWER_LIMIT = 0x2C09, // Scaling Lower Limit: Lower limit for scaling input. + OR_E5_SWR_SCALING_UPPER_LIMIT = 0x2C0B, // Scaling Upper Limit: Upper limit for scaling input. + OR_E5_SWR_DECIMAL_POINT = 0x2C0C, // Decimal Point: Decimal point display settings. + OR_E5_SWR_REMOTE_SP_UPPER_LIMIT = 0x2C0D, // Remote SP Upper Limit: Upper limit for remote SP input. + OR_E5_SWR_REMOTE_SP_LOWER_LIMIT = 0x2C0E, // Remote SP Lower Limit: Lower limit for remote SP input. + OR_E5_SWR_PV_DECIMAL_POINT_DISPLAY = 0x2C0F, // PV Decimal Point Display: Display settings for PV decimal points. + + //--- Continuation from previous entries... + OR_E5_SWR_CONTROL_OUTPUT_1_SIGNAL = 0x2D03, // Control Output 1 Signal: Signal settings for the first control output. + OR_E5_SWR_CONTROL_OUTPUT_2_SIGNAL = 0x2D04, // Control Output 2 Signal: Signal settings for the second control output. + OR_E5_SWR_SP_UPPER_LIMIT = 0x2D0F, // SP Upper Limit: Upper limit for set point range. + OR_E5_SWR_SP_LOWER_LIMIT = 0x2D10, // SP Lower Limit: Lower limit for set point range. + OR_E5_SWR_STANDARD_OR_HEATING_COOLING = 0x2D11, // Standard or Heating/Cooling: Operation mode selection. + OR_E5_SWR_DIRECT_REVERSE_OPERATION = 0x2D12, // Direct/Reverse Operation: Operation direction setting. + OR_E5_SWR_CLOSE_FLOATING = 0x2D13, // Close/Floating: Selection between close and floating control. + OR_E5_SWR_PID_ON_OFF = 0x2D14, // PID ON/OFF: PID control on/off switch. + OR_E5_SWR_ST = 0x2D15, // ST: Special function toggle. + OR_E5_SWR_PROGRAM_PATTERN = 0x2D16, // Program Pattern: Program pattern settings. + OR_E5_SWR_REMOTE_SP_INPUT = 0x2D18, // Remote SP Input: Remote set point input settings. + OR_E5_SWR_MINIMUM_OUTPUT_ON_OFF_BAND = 0x2D19, // Minimum Output ON/OFF Band: Minimum output settings for ON/OFF control. + OR_E5_SWR_TRANSFER_OUTPUT_TYPE = 0x2E00, // Transfer Output Type: Output type settings for data transfer. + OR_E5_SWR_TRANSFER_OUTPUT_SIGNAL = 0x2E01, // Transfer Output Signal: Signal settings for transfer output. + OR_E5_SWR_CONTROL_OUTPUT_1_ASSIGNMENT = 0x2E06, // Control Output 1 Assignment: Assignment settings for the first control output. + OR_E5_SWR_CONTROL_OUTPUT_2_ASSIGNMENT = 0x2E07, // Control Output 2 Assignment: Assignment settings for the second control output. + OR_E5_SWR_EVENT_INPUT_ASSIGNMENT_1 = 0x2E0A, // Event Input Assignment 1: Event input settings for the first input. + OR_E5_SWR_EVENT_INPUT_ASSIGNMENT_2 = 0x2E0B, // Event Input Assignment 2: Event input settings for the second input. + OR_E5_SWR_EVENT_INPUT_ASSIGNMENT_3 = 0x2E0C, // Event Input Assignment 3: Event input settings for the third input. + OR_E5_SWR_EVENT_INPUT_ASSIGNMENT_4 = 0x2E0D, // Event Input Assignment 4: Event input settings for the fourth input. + OR_E5_SWR_EVENT_INPUT_ASSIGNMENT_5 = 0x2E0E, // Event Input Assignment 5: Event input settings for the fifth input. + OR_E5_SWR_EVENT_INPUT_ASSIGNMENT_6 = 0x2E0F, // Event Input Assignment 6: Event input settings for the sixth input. + OR_E5_SWR_AUXILIARY_OUTPUT_1_ASSIGNMENT = 0x2E10, // Auxiliary Output 1 Assignment: Assignment settings for auxiliary output 1. + OR_E5_SWR_AUXILIARY_OUTPUT_2_ASSIGNMENT = 0x2E11, // Auxiliary Output 2 Assignment: Assignment settings for auxiliary output 2. + OR_E5_SWR_AUXILIARY_OUTPUT_3_ASSIGNMENT = 0x2E12, // Auxiliary Output 3 Assignment: Assignment settings for auxiliary output 3. + OR_E5_SWR_AUXILIARY_OUTPUT_4_ASSIGNMENT = 0x2E13, // Auxiliary Output 4 Assignment: Assignment settings for auxiliary output 4. + OR_E5_SWR_TRANSFER_OUTPUT_UPPER_LIMIT = 0x2E14, // Transfer Output Upper Limit: Upper limit settings for transfer output. + OR_E5_SWR_TRANSFER_OUTPUT_LOWER_LIMIT = 0x2E15, // Transfer Output Lower Limit: Lower limit settings for transfer output. + OR_E5_SWR_SIMPLE_TRANSFER_OUTPUT_1_UPPER_LIMIT = 0x2E16, // Simple Transfer Output 1 Upper Limit: Upper limit for simplified transfer output 1. + OR_E5_SWR_SIMPLE_TRANSFER_OUTPUT_1_LOWER_LIMIT = 0x2E17, + + // --- Continuation from previous entries... + OR_E5_SWR_EXTRACTION_OF_SQUARE_ROOT_ENABLE = 0x2E24, // Extraction of Square Root Enable: Enable/disable extraction of square root. + OR_E5_SWR_TRAVEL_TIME = 0x2E30, // Travel Time: Settings for the duration of travel time. + OR_E5_SWR_ALARM_1_TYPE = 0x2F00, // Alarm 1 Type: + + // --- Continuation from previous entries... + OR_E5_SWR_ALARM_1_TYPE = 0x2F00, // Alarm 1 Type: Type settings for the first alarm. + OR_E5_SWR_ALARM_1_LATCH = 0x2F01, // Alarm 1 Latch: Latch settings for the first alarm. + OR_E5_SWR_ALARM_1_HYSTERESIS = 0x2F02, // Alarm 1 Hysteresis: Hysteresis settings for the first alarm. + OR_E5_SWR_ALARM_2_TYPE = 0x2F03, // Alarm 2 Type: Type settings for the second alarm. + OR_E5_SWR_ALARM_2_LATCH = 0x2F04, // Alarm 2 Latch: Latch settings for the second alarm. + OR_E5_SWR_ALARM_2_HYSTERESIS = 0x2F05, // Alarm 2 Hysteresis: Hysteresis settings for the second alarm. + OR_E5_SWR_ALARM_3_TYPE = 0x2F06, // Alarm 3 Type: Type settings for the third alarm. + OR_E5_SWR_ALARM_3_LATCH = 0x2F07, // Alarm 3 Latch: Latch settings for the third alarm. + OR_E5_SWR_ALARM_3_HYSTERESIS = 0x2F08, // Alarm 3 Hysteresis: Hysteresis settings for the third alarm. + OR_E5_SWR_ALARM_4_TYPE = 0x2F09, // Alarm 4 Type: Type settings for the fourth alarm. + OR_E5_SWR_ALARM_4_LATCH = 0x2F0A, // Alarm 4 Latch: Latch settings for the fourth alarm. + OR_E5_SWR_ALARM_4_HYSTERESIS = 0x2F0B, // Alarm 4 Hysteresis: Hysteresis settings for the fourth alarm. + OR_E5_SWR_STANDBY_SEQUENCE_RESET = 0x2F0C, // Standby Sequence Reset: Reset settings for the standby sequence. + OR_E5_SWR_AUXILIARY_OUTPUT_1_OPEN_IN_ALARM = 0x2F0D, // Auxiliary Output 1 Open in Alarm: Open settings for auxiliary output 1 when in alarm. + OR_E5_SWR_AUXILIARY_OUTPUT_2_OPEN_IN_ALARM = 0x2F0E, // Auxiliary Output 2 Open in Alarm: Open settings for auxiliary output 2 when in alarm. + OR_E5_SWR_AUXILIARY_OUTPUT_3_OPEN_IN_ALARM = 0x2F0F, // Auxiliary Output 3 Open in Alarm: Open settings for auxiliary output 3 when in alarm. + OR_E5_SWR_AUXILIARY_OUTPUT_4_OPEN_IN_ALARM = 0x2F10, // Auxiliary Output 4 Open in Alarm: Open settings for auxiliary output 4 when in alarm. + OR_E5_SWR_ALARM_1_ON_DELAY = 0x2F11, // Alarm 1 ON Delay: ON delay settings for the first alarm. + OR_E5_SWR_ALARM_2_ON_DELAY = 0x2F12, // Alarm 2 ON Delay: ON delay settings for the second alarm. + OR_E5_SWR_ALARM_3_ON_DELAY = 0x2F13, // Alarm 3 ON Delay: ON delay settings for the third alarm. + OR_E5_SWR_ALARM_4_ON_DELAY = 0x2F14, // Alarm 4 ON Delay: ON delay settings for the fourth alarm. + OR_E5_SWR_ALARM_1_OFF_DELAY = 0x2F15, // Alarm 1 OFF Delay: OFF delay settings for the first alarm. + OR_E5_SWR_ALARM_2_OFF_DELAY = 0x2F16, // Alarm 2 OFF Delay: OFF delay settings for the second alarm. + OR_E5_SWR_ALARM_3_OFF_DELAY = 0x2F17, // Alarm 3 OFF Delay: OFF delay settings for the third alarm. + OR_E5_SWR_ALARM_4_OFF_DELAY = 0x2F18, // Alarm 4 OFF Delay: OFF delay settings for the fourth alarm. + OR_E5_SWR_PV_SP_NO_1_DISPLAY_SELECTION = 0x3000, // PV/SP No. 1 Display Selection: Display settings for PV/SP No. 1. + OR_E5_SWR_MV_DISPLAY_SELECTION = 0x3001, // MV Display Selection: Display settings for MV. + + + // --- Continuation from previous entries... + OR_E5_SWR_AUTOMATIC_DISPLAY_RETURN_TIME = 0x3003, // Automatic Display Return Time: Time settings for automatic display return. + OR_E5_SWR_DISPLAY_REFRESH_PERIOD = 0x3004, // Display Refresh Period: Settings for the refresh period of the display. + OR_E5_SWR_PV_SP_NO_2_DISPLAY_SELECTION = 0x3008, // PV/SP No. 2 Display Selection: Display settings for PV/SP No. 2. + OR_E5_SWR_VALVE_OPENING_MONITOR_SELECTION = 0x3009, // Valve Opening Monitor Selection: Selection settings for valve opening monitoring. + OR_E5_SWR_DISPLAY_BRIGHTNESS = 0x300A, // Display Brightness: Brightness settings for the display. + OR_E5_SWR_MV_DISPLAY = 0x300B, // MV Display: Display settings for MV. + OR_E5_SWR_MOVE_TO_PROTECT_LEVEL_TIME = 0x300C, // Move to Protect Level Time: Time settings for moving to protect level. + OR_E5_SWR_AUTO_MANUAL_SELECT_ADDITION = 0x300F, // Auto/Manual Select Addition: Settings for adding auto/manual selection. + OR_E5_SWR_PV_STATUS_DISPLAY_FUNCTION = 0x3011, // PV Status Display Function: Function settings for displaying PV status. + OR_E5_SWR_SV_STATUS_DISPLAY_FUNCTION = 0x3012, // SV Status Display Function: Function settings for displaying SV status. + OR_E5_SWR_PROTOCOL_SETTING = 0x3100, // Protocol Setting: Settings for communication protocol. + OR_E5_SWR_COMMUNICATIONS_UNIT_NO = 0x3101, // Communications Unit No.: Unit number settings for communications. + OR_E5_SWR_COMMUNICATIONS_BAUD_RATE = 0x3102, // Communications Baud Rate: Baud rate settings for communications. + OR_E5_SWR_COMMUNICATIONS_DATA_LENGTH = 0x3103, // Communications Data Length: Data length settings for communications. + OR_E5_SWR_COMMUNICATIONS_STOP_BITS = 0x3104, // Communications Stop Bits: Stop bit settings for communications. + OR_E5_SWR_COMMUNICATIONS_PARITY = 0x3105, // Communications Parity: Parity settings for communications. + OR_E5_SWR_SEND_DATA_WAIT_TIME = 0x3106, // Send Data Wait Time: Wait time settings for sending data. + OR_E5_SWR_PF_SETTING = 0x3200, // PF Setting: Settings for PF function. + OR_E5_SWR_MONITOR_SETTING_ITEM_1 = 0x3202, // Monitor/Setting Item 1: Settings for monitor/item 1. + OR_E5_SWR_MONITOR_SETTING_ITEM_2 = 0x3203, // Monitor/Setting Item 2: Settings for monitor/item 2. + OR_E5_SWR_MONITOR_SETTING_ITEM_3 = 0x3204, // Monitor/Setting Item 3: Settings for monitor/item 3. + OR_E5_SWR_MONITOR_SETTING_ITEM_4 = 0x3205, // Monitor/Setting Item 4: Settings for monitor/item 4. + OR_E5_SWR_MONITOR_SETTING_ITEM_5 = 0x3206, // Monitor/Setting Item 5: Settings for monitor/item 5. + OR_E5_SWR_SP_TRACKING = 0x3301, // SP Tracking: Settings for set point tracking. + OR_E5_SWR_PV_DEAD_BAND = 0x3304, // PV Dead Band: Dead band settings for PV. + OR_E5_SWR_COLD_JUNCTION_COMPENSATION_METHOD = 0x3305,// Cold Junction Compensation Method: Method settings for cold junction compensation. + OR_E5_SWR_INTEGRAL_DERIVATIVE_TIME_UNIT = 0x3309, // Integral/Derivative Time Unit: Time unit settings for integral/derivative. + OR_E5_SWR_ALPHA = 0x330A, // Alpha: Settings for alpha parameter. + OR_E5_SWR_MANUAL_OUTPUT_METHOD = 0x330C, // Manual Output Method: Method settings for manual output. + OR_E5_SWR_MANUAL_MV_INITIAL_VALUE = 0x330D, // Manual MV Initial Value: Initial value settings for manual MV. + OR_E5_SWR_AT_CALCULATED_GAIN = 0x330F, // AT Calculated Gain: Gain settings for auto-tuning. + OR_E5_SWR_AT_HYSTERESIS = 0x3310, // AT + + //-- Continuation from previous entries... + OR_E5_SWR_AUTOMATIC_DISPLAY_RETURN_TIME = 0x3003, // Automatic Display Return Time: Time settings for automatic return to the display. + OR_E5_SWR_DISPLAY_REFRESH_PERIOD = 0x3004, // Display Refresh Period: Settings for how often the display refreshes. + OR_E5_SWR_PV_SP_NO_2_DISPLAY_SELECTION = 0x3008, // PV/SP No. 2 Display Selection: Display settings for PV/SP No. 2. + OR_E5_SWR_VALVE_OPENING_MONITOR_SELECTION = 0x3009, // Valve Opening Monitor Selection: Selection of valve opening monitoring mode. + OR_E5_SWR_DISPLAY_BRIGHTNESS = 0x300A, // Display Brightness: Brightness settings for the display. + OR_E5_SWR_MV_DISPLAY = 0x300B, // MV Display: Display settings for MV. + OR_E5_SWR_MOVE_TO_PROTECT_LEVEL_TIME = 0x300C, // Move to Protect Level Time: Time settings to move to protect level. + OR_E5_SWR_AUTO_MANUAL_SELECT_ADDITION = 0x300F, // Auto/Manual Select Addition: Settings for adding auto/manual selection. + OR_E5_SWR_PV_STATUS_DISPLAY_FUNCTION = 0x3011, // PV Status Display Function: Function settings for displaying PV status. + OR_E5_SWR_SV_STATUS_DISPLAY_FUNCTION = 0x3012, // SV Status Display Function: Function settings for displaying SV status. + OR_E5_SWR_PROTOCOL_SETTING = 0x3100, // Protocol Setting: Settings for the communication protocol. + OR_E5_SWR_COMMUNICATIONS_UNIT_NO = 0x3101, // Communications Unit No.: Unit number settings for communications. + OR_E5_SWR_COMMUNICATIONS_BAUD_RATE = 0x3102, // Communications Baud Rate: Baud rate settings for communications. + OR_E5_SWR_COMMUNICATIONS_DATA_LENGTH = 0x3103, // Communications Data Length: Data length settings for communications. + OR_E5_SWR_COMMUNICATIONS_STOP_BITS = 0x3104, // Communications Stop Bits: Stop bit settings for communications. + OR_E5_SWR_COMMUNICATIONS_PARITY = 0x3105, // Communications Parity: Parity settings for communications. + OR_E5_SWR_SEND_DATA_WAIT_TIME = 0x3106, // Send Data Wait Time: Wait time settings for sending data. + OR_E5_SWR_PF_SETTING = 0x3200, // PF Setting: Settings for PF functions. + OR_E5_SWR_MONITOR_SETTING_ITEM_1 = 0x3202, // Monitor/Setting Item 1: Settings for monitoring and adjustment for item 1. + OR_E5_SWR_MONITOR_SETTING_ITEM_2 = 0x3203, // Monitor/Setting Item 2: Settings for monitoring and adjustment for item 2. + OR_E5_SWR_MONITOR_SETTING_ITEM_3 = 0x3204, // Monitor/Setting Item 3: Settings for monitoring and adjustment for item 3. + OR_E5_SWR_MONITOR_SETTING_ITEM_4 = 0x3205, // Monitor/Setting Item 4: Settings for monitoring and adjustment for item 4. + OR_E5_SWR_MONITOR_SETTING_ITEM_5 = 0x3206, // Monitor/Setting Item 5: Settings for monitoring and adjustment for item 5. + OR_E5_SWR_SP_TRACKING = 0x3301, // SP Tracking: Settings for tracking set points. + OR_E5_SWR_PV_DEAD_BAND = 0x3304, // PV Dead Band: Settings for the PV dead band. + OR_E5_SWR_COLD_JUNCTION_COMPENSATION_METHOD = 0x3305, // Cold Junction Compensation Method: Settings for compensating the cold junction. + OR_E5_SWR_INTEGRAL_DERIVATIVE_TIME_UNIT = 0x3309, // Integral/Derivative Time Unit: Time unit settings for integral and derivative calculations. + OR_E5_SWR_ALPHA = 0x330A, // α: Settings for the α parameter in control equations. + OR_E5_SWR_MANUAL_OUTPUT_METHOD = 0x330C, // Manual Output Method: Settings for manual output methods. + OR_E5_SWR_MANUAL_MV_INITIAL_VALUE = 0x330D, // Manual MV Initial Value: Initial value settings for manual MV. + OR_E5_SWR_AT_CALCULATED_GAIN = 0x330F, // AT Calculated Gain: Settings for automatically calculated + + // Continuation from previous entries... + OR_E5_SWR_LCT_COOLING_OUTPUT_MINIMUM_ON_TIME = 0x3335, // LCT Cooling Output Minimum ON Time: Minimum ON time settings for LCT cooling output. + + + +}; + + +#endif // OMRONE5_EX_H \ No newline at end of file diff --git a/src/components/POT.h b/src/components/POT.h new file mode 100644 index 00000000..dbbdbd43 --- /dev/null +++ b/src/components/POT.h @@ -0,0 +1,346 @@ +#ifndef POT_H +#define POT_H + +#include // millis(), pinMode(), analogRead(), map() +#include +#include +#include +#include +#include "config.h" +#include "config-modbus.h" +#include "../modbus/Modbus.h" +#include "../modbus/ModbusTCP.h" + +/* + * Hardware Smoothing Suggestion (RC Low-Pass Filter): + * + * @link : https://docs.espressif.com/projects/esp-idf/en/v4.4/esp32/api-reference/peripherals/adc.html + * To further reduce noise on the analog input signal before it reaches the ADC, + * a simple RC low-pass filter can be implemented using the potentiometer itself + * as the resistor (R) and adding a capacitor (C) between the potentiometer's + * wiper output and ground. + * + * Potentiometer Resistance (R): 5 kΩ (5000 Ω) + * Input Voltage: 5V + * + * Cutoff Frequency (f_c) = 1 / (2 * π * R * C) + * Capacitor (C) = 1 / (2 * π * R * f_c) + * + * Example Capacitor Values for R = 5 kΩ: + * - For f_c ≈ 30 Hz (Faster response): C ≈ 1 / (2 * π * 5000 * 30) ≈ 1.06 µF -> Use 1 µF + * - For f_c ≈ 10 Hz (Moderate): C ≈ 1 / (2 * π * 5000 * 10) ≈ 3.18 µF -> Use 2.2 µF or 3.3 µF + * - For f_c ≈ 5 Hz (More smoothing): C ≈ 1 / (2 * π * 5000 * 5) ≈ 6.37 µF -> Use 4.7 µF or 6.8 µF + * + * Recommendation: Start with C = 1 µF to 3.3 µF and adjust based on observed + * noise reduction and response time requirements. Use a ceramic capacitor + * rated for at least the input voltage (e.g., 10V or 16V). + */ + +/* -------------------------------------------------------------------------- + * POT –– Analogue potentiometer reader with lightweight digital filtering + * -------------------------------------------------------------------------- + * ▸ Supports three damping modes: NONE, MOVING‑AVERAGE (box‑car) and EMA. + * ▸ Moving‑average implemented as an incremental ring buffer (O(1)). + * ▸ EMA uses a 1‑pole IIR: y[n] = y[n‑1] + (x[n] − y[n‑1]) / 2^k. + * ▸ Dead‑band suppresses ±1‑LSB chatter after scaling to 0‑100. + * ▸ Designed for small AVR/ESP32 class MCUs – no malloc, no floats. + * ----------------------------------------------------------------------- */ + +// ------------------------------------------------------------------------- +// Compile‑time knobs (override in config.h if desired) +// ------------------------------------------------------------------------- +#ifndef POT_SAMPLE_INTERVAL +#define POT_SAMPLE_INTERVAL 10 // ms between ADC reads +#endif + +#ifndef POT_DA_WIN_LEN +#define POT_DA_WIN_LEN 8 // box‑car window length (must be power‑of‑two) +#endif + +// Compile-time check for power-of-two window length for moving average +static_assert((POT_DA_WIN_LEN > 0) && ((POT_DA_WIN_LEN & (POT_DA_WIN_LEN - 1)) == 0), + "POT_DA_WIN_LEN must be a power of two for efficient moving average calculation."); + +#ifndef POT_DA_EMA_SHIFT +#define POT_DA_EMA_SHIFT 3 // alpha = 1 / (2^shift) (shift = 3 ⇒ α = 0.125) +#endif + +#ifndef POT_DEADBAND +#define POT_DEADBAND 1 // change (0‑100 scale) required to notify +#endif + +#ifndef POT_RAW_MAX_VALUE +#define POT_RAW_MAX_VALUE 1023 // 10‑bit ADC full‑scale +#endif + +#ifndef POT_SCALED_MAX_VALUE +#define POT_SCALED_MAX_VALUE 100 // application‑level full‑scale +#endif + +#ifndef POT_DAMPING_WINDOW_SIZE +#define POT_DAMPING_WINDOW_SIZE POT_DA_WIN_LEN // alias for legacy code +#endif + +// ------------------------------------------------------------------------- +// Damping algorithms +// ------------------------------------------------------------------------- +enum class POTDampingAlgorithm : uint8_t +{ + DAMPING_NONE = 0, + DAMPING_MOVING_AVERAGE, + DAMPING_EMA +}; + +// ------------------------------------------------------------------------- +// Control modes +// ------------------------------------------------------------------------- +enum class E_POTControlMode : uint8_t +{ + E_AUX_LOCAL = 0, + E_AUX_REMOTE = 1 +}; + +// Address offsets for Modbus registers +enum E_POT_REGISTER_OFFSET : uint16_t +{ + OFFSET_VALUE = 0, + OFFSET_MODE = 1, + OFFSET_REMOTE_VALUE = 2 +}; + +class Bridge; +class POT : public Component +{ +public: + POT(Component *owner, + uint16_t _pin, + uint16_t _id, + uint16_t _modbusAddress, + POTDampingAlgorithm _algo = POTDampingAlgorithm::DAMPING_NONE) + : Component("POT", _id, Component::COMPONENT_DEFAULT, owner), + pin(_pin), + modbusAddress(_modbusAddress), + algo(_algo) + { + setNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS); + + // Initialize instance-specific Modbus blocks. + // INIT_MODBUS_BLOCK is assumed to use `this->modbusAddress` and potentially `this->id` + // (e.g., for slaveId if that's how it's intended) from the current POT instance. + m_modbus_blocks[0] = INIT_MODBUS_BLOCK_TCP(modbusAddress, OFFSET_VALUE, E_FN_CODE::FN_READ_HOLD_REGISTER, + MB_ACCESS_READ_ONLY, "POT Value", "aux"); + m_modbus_blocks[1] = INIT_MODBUS_BLOCK_TCP(modbusAddress, OFFSET_MODE, E_FN_CODE::FN_READ_HOLD_REGISTER, + MB_ACCESS_READ_WRITE, "POT Mode (0=Local,1=Remote)", "aux"); + m_modbus_blocks[2] = INIT_MODBUS_BLOCK_TCP(modbusAddress, OFFSET_REMOTE_VALUE, E_FN_CODE::FN_READ_HOLD_REGISTER, + MB_ACCESS_READ_WRITE, "POT Remote Value", "aux"); + + // Initialize the view to point to these instance-specific blocks + m_modbus_view.data = m_modbus_blocks; // m_modbus_blocks is MB_Registers[3], so this is MB_Registers* + m_modbus_view.count = sizeof(m_modbus_blocks) / sizeof(m_modbus_blocks[0]); + + } + + // --------------------------------------------------- lifecycle ------- + short setup() override + { + Component::setup(); + pinMode(pin, INPUT); + lastSample = millis(); + return E_OK; + } + + short loop() override + { + uint32_t now = millis(); + if (now - lastSample < POT_SAMPLE_INTERVAL) return E_OK; + lastSample = now; + uint16_t raw = analogRead(pin); + // ---------------------- filtering ------------------------------- + switch (algo) + { + case POTDampingAlgorithm::DAMPING_MOVING_AVERAGE: + acc -= ring[idx]; + acc += raw; + ring[idx] = raw; + idx = (idx + 1) & (POT_DA_WIN_LEN - 1); // power‑of‑two length + filtRaw = acc >> log2_WIN_LEN; // divide by window length + break; + + case POTDampingAlgorithm::DAMPING_EMA: + // y += (x - y) / 2^k -- shift = k + filtRaw += (raw - filtRaw) >> POT_DA_EMA_SHIFT; + break; + + case POTDampingAlgorithm::DAMPING_NONE: + default: + filtRaw = raw; + break; + } + + // ------------------- scale & dead‑band --------------------------- + uint16_t scaledLocalValue = map(filtRaw, 0, POT_RAW_MAX_VALUE, 0, POT_SCALED_MAX_VALUE); + uint16_t newValue = value; // Start with current value + + if (controlMode == E_POTControlMode::E_AUX_LOCAL) + { + // LOCAL Mode: Use filtered/scaled value with deadband + if (abs(int16_t(scaledLocalValue) - int16_t(value)) > POT_DEADBAND) + { + newValue = scaledLocalValue; + } + } + else // E_AUX_REMOTE Mode + { + newValue = remoteValue; // Already clamped in mb_tcp_write + } + + if (newValue != value) + { + value = newValue; + notifyStateChange(); + } + + return E_OK; + } + + // --------------------------------------------------- diagnostics ---- + short debug() override { return info(); } + + short info(short = 0, short = 0) override + { + Log.verboseln("POT::info - ID:%d Pin:%d Val:%d Algo:%d Mode:%d RemoteVal:%d Raw:%d Address:%d", + id, pin, value, static_cast(algo), + static_cast(controlMode), remoteValue, analogRead(pin), modbusAddress); + + return E_OK; + } + + // --------------------------------------------------- Modbus --------- + short mb_tcp_write(MB_Registers* reg, short networkValue) override + { + uint16_t addr = reg->startAddress; + bool changed = false; + + if (addr == modbusAddress + OFFSET_MODE) // Write to Mode register + { + E_POTControlMode newMode = (networkValue == 0) ? E_POTControlMode::E_AUX_LOCAL : E_POTControlMode::E_AUX_REMOTE; + if (newMode != controlMode) + { + Log.verboseln("POT::mb_write - ID:%d Mode change %d -> %d", id, static_cast(controlMode), static_cast(newMode)); + controlMode = newMode; + changed = true; + } + } + else if (addr == modbusAddress + OFFSET_REMOTE_VALUE) // Write to Remote Value register + { + // Clamp remote value to valid scaled range (0-100 or defined max) + uint16_t clampedValue = constrain(networkValue, 0, POT_SCALED_MAX_VALUE); + if (clampedValue != remoteValue) + { + Log.verboseln("POT::mb_write - ID:%d Remote value change %d -> %d", id, remoteValue, clampedValue); + remoteValue = clampedValue; + if (controlMode == E_POTControlMode::E_AUX_REMOTE) { + changed = true; // Trigger update in loop if remote + } + } + } + else if (addr == modbusAddress + OFFSET_VALUE) // Writing to Value register (read-only) + { + // Writing to the main value register is not allowed directly + return MODBUS_ERROR_ILLEGAL_FUNCTION; + } + else + { + return E_INVALID_PARAMETER; // Address doesn't match any known register + } + + // If in remote mode and either mode or remote value changed, update main value immediately + if (changed && controlMode == E_POTControlMode::E_AUX_REMOTE) + { + if (value != remoteValue) + { + value = remoteValue; + notifyStateChange(); + } + } + // If mode changed to local, next loop will update value based on pot + + return E_OK; + } + + short mb_tcp_read(MB_Registers* reg) override + { + uint16_t addr = reg->startAddress; + if (addr == modbusAddress + OFFSET_VALUE) { + return value; + } + else if (addr == modbusAddress + OFFSET_MODE) { // Read Mode + return static_cast(controlMode); + } + else if (addr == modbusAddress + OFFSET_REMOTE_VALUE) { // Read Remote Value + return remoteValue; + } + return 0; // Default or error for unknown address + } + + void mb_tcp_register(ModbusTCP *mgr) const override + { + ModbusBlockView* view = mb_tcp_blocks(); + for (int i=0; i < view->count; ++i) + { + mgr->registerModbus(const_cast(this), view->data[i]); // view->data[i] is MB_Registers + } + } + + // Returns a pointer to the instance-specific Modbus block definitions. + // The virtual function in Component base class is likely `virtual ModbusBlockView* mb_tcp_blocks() const;` + ModbusBlockView* mb_tcp_blocks() const override + { + // m_modbus_view is initialized in constructor and doesn't change. + // Returning a non-const pointer from a const method is allowed if the member is mutable. + return &m_modbus_view; + } + + short serial_register(Bridge *b) override + { + b->registerMemberFunction(id, this, C_STR("info"), (ComponentFnPtr)&POT::info); + return E_OK; + } + + // --------------------------------------------------- public helpers - + uint16_t getValue() const { return value; } + +private: + // ----------------------------- constants ---------------------------- + static constexpr uint8_t log2_WIN_LEN = + (POT_DA_WIN_LEN == 1) ? 0 : + (POT_DA_WIN_LEN == 2) ? 1 : + (POT_DA_WIN_LEN == 4) ? 2 : + (POT_DA_WIN_LEN == 8) ? 3 : + (POT_DA_WIN_LEN == 16)? 4 : 0; // extend if larger windows used + + // ----------------------------- members ------------------------------ + const uint16_t pin; + const uint16_t modbusAddress; + const POTDampingAlgorithm algo; + + // --- Control Mode --- + E_POTControlMode controlMode = E_POTControlMode::E_AUX_LOCAL; + uint16_t remoteValue = 0; // Value set via Modbus in E_AUX_REMOTE mode + // --------------------- + + uint16_t ring[POT_DA_WIN_LEN] = {0}; // circular buffer + uint32_t acc = 0; // running sum + uint8_t idx = 0; // buffer index + uint16_t filtRaw = 0; // filtered raw sample + uint16_t value = 0; // scaled, debounced value (0‑100) + + uint32_t lastSample = 0; // ms + + // Instance-specific storage for Modbus block definitions + MB_Registers m_modbus_blocks[3]; + // m_modbus_view needs to be mutable to be returned as ModbusBlockView* from a const method. + mutable ModbusBlockView m_modbus_view; +}; + +#endif // POT_H diff --git a/src/components/Plunger.cpp b/src/components/Plunger.cpp new file mode 100644 index 00000000..4688e14d --- /dev/null +++ b/src/components/Plunger.cpp @@ -0,0 +1,759 @@ +#include "Plunger.h" +#include +#include "PlungerSettings.h" + +const bool debug_jam = true; +const bool debug_states = true; + +const char *_plungerStateToString(PlungerState state) +{ + switch (state) + { + case PlungerState::IDLE: + return "IDLE"; + case PlungerState::HOMING_MANUAL: + return "HOMING_MANUAL"; + case PlungerState::HOMING_AUTO: + return "HOMING_AUTO"; + case PlungerState::PLUNGING_MANUAL: + return "PLUNGING_MANUAL"; + case PlungerState::PLUNGING_AUTO: + return "PLUNGING_AUTO"; + case PlungerState::STOPPING: + return "STOPPING"; + case PlungerState::JAMMED: + return "JAMMED"; + case PlungerState::RESETTING_JAM: + return "RESETTING_JAM"; + case PlungerState::RECORD: + return "RECORD"; + case PlungerState::REPLAY: + return "REPLAY"; + case PlungerState::FILLING: + return "FILLING"; + case PlungerState::POST_FLOW: + return "POST_FLOW"; + default: + return "UNKNOWN_STATE"; + } +} + +// Re-add _fillStateToString +const char *_fillStateToString(FillState state) +{ + switch (state) + { + case FillState::NONE: + return "NONE"; + case FillState::PLUNGING: + return "PLUNGING"; + case FillState::PLUNGED: + return "PLUNGED"; + case FillState::HOMING: + return "HOMING"; + case FillState::HOMED: + return "HOMED"; + default: + return "UNKNOWN_FILL_STATE"; + } +} + +// Add _postFlowStateToString +const char *_postFlowStateToString(PostFlowState state) +{ + switch (state) + { + case PostFlowState::NONE: + return "NONE"; + case PostFlowState::POST_FLOW_STOPPING: + return "STOPPING"; + case PostFlowState::POST_FLOW_STARTING: + return "STARTING"; + case PostFlowState::POST_FLOW_COMPLETE: + return "COMPLETE"; + default: + return "UNKNOWN_POST_FLOW_STATE"; + } +} + +Plunger::Plunger(Component *owner, SAKO_VFD *vfd, Joystick *joystick, POT *speedPot, POT *torquePot) + : Component(PLUNGER_COMPONENT_NAME, COMPONENT_KEY_PLUNGER, Component::COMPONENT_DEFAULT, owner), + _vfd(vfd), + _joystick(joystick), + _speedPot(speedPot), + _torquePot(torquePot), + _currentState(PlungerState::IDLE), + _currentFillState(FillState::NONE), + _lastJoystickDirection(Joystick::E_POSITION::CENTER), + _currentSpeedPotValue(0), + _currentTorquePotValue(0), + _calculatedPlungingSpeedHz(0), + _lastStateChangeTimeMs(0), + _jammedStartTimeMs(0), + _lastVfdReadTimeMs(0), + _joystickHoldStartTimeMs(0), + _modbusCommandRegisterValue(static_cast(E_PlungerCommand::NO_COMMAND)), + _operationStartTimeMs(0), + _currentMaxOperationTimeMs(0), + _joystickReleasedSinceAutoStart(false), + _autoModeEnabled(true), + _lastDiagnosticLogTimeMs(0), + _lastImmediateStopCheckTimeMs(0), + _lastStateLogTimeMs(0), + _recordedPlungeDurationMs(0), + _recordModeStartTimeMs(0), + _fillOperationStartTimeMs(0), + _postFlowStartTimeMs(0), + _currentPostFlowState(PostFlowState::NONE) +{ + setNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS); +} + +short Plunger::init() +{ + _vfdResetJam(); + _vfdStop(); + _transitionToState(PlungerState::IDLE); + _lastJoystickDirection = Joystick::E_POSITION::CENTER; + _lastStateChangeTimeMs = millis(); + _jammedStartTimeMs = 0; + _lastVfdReadTimeMs = 0; + _joystickHoldStartTimeMs = 0; + _operationStartTimeMs = 0; + _currentMaxOperationTimeMs = 0; + _lastDiagnosticLogTimeMs = 0; + _lastImmediateStopCheckTimeMs = 0; + _lastStateLogTimeMs = 0; + _modbusCommandRegisterValue = static_cast(E_PlungerCommand::NO_COMMAND); + _joystickReleasedSinceAutoStart = false; + _autoModeEnabled = true; + + bool loadSettings = true; + + if (loadSettings) + { + // _settings is already initialized with compile-time defaults by its constructor. + // Now, attempt to load from file, overriding defaults if successful. + Log.infoln("[%s] init() called. Attempting to load settings...", name.c_str()); + if (_settings.load()) + { // Uses default path "/plunger.json" + Log.infoln("[%s] Settings loaded successfully from file during init.", name.c_str()); + } + else + { + Log.warningln("[%s] Could not load settings from file during init, using compile-time defaults. Attempting to save defaults to create file.", name.c_str()); + if (_settings.save()) + { // Attempt to save the current (compile-time default) settings + Log.infoln("[%s] Default settings saved to file during init.", name.c_str()); + } + else + { + Log.errorln("[%s] Failed to save default settings to file during init.", name.c_str()); + } + } + } + + // Now that settings are finalized (loaded or compile-time defaults), proceed with dependent initializations. + _updatePotValues(); // Recalculates _calculatedPlungingSpeedHz based on potentially loaded _settings.speedFastHz + _recordedPlungeDurationMs = _settings.replayDurationMs; + + _recordModeStartTimeMs = 0; + _joystickRecordHoldTimer.detach(); + _replayPlungeTimer.detach(); + + _currentFillState = FillState::NONE; + _fillOperationStartTimeMs = 0; + _fillSubStateTimer.detach(); + _joystickFillHoldTimer.detach(); + + _postFlowStartTimeMs = 0; + _currentPostFlowState = PostFlowState::NONE; + _postFlowSubStateTimer.detach(); + + Log.infoln("[%s] Final Plunger settings after init process:", name.c_str()); + _settings.print(); // Log the finalized settings + + return E_OK; +} + +short Plunger::setup() +{ + Component::setup(); + return this->init(); +} + +void Plunger::_updatePotValues() +{ + _currentSpeedPotValue = _speedPot->getValue(); + _currentTorquePotValue = _torquePot->getValue(); + _calculatedPlungingSpeedHz = (_currentSpeedPotValue / 100.0f) * (_settings.speedFastHz * 100.0f); +} + +void Plunger::_vfdStartForward(uint16_t frequencyCentiHz) +{ + _vfd->setFrequency(frequencyCentiHz / 100); // SAKO_VFD::setFrequency expects Hz + _vfd->run(); +} + +void Plunger::_vfdStartReverse(uint16_t frequencyCentiHz) +{ + _vfd->setFrequency(frequencyCentiHz / 100); // SAKO_VFD::setFrequency expects Hz + _vfd->reverse(); +} + +void Plunger::_vfdStop() +{ + _vfd->stop(); +} + +void Plunger::_vfdResetJam() +{ + Log.verboseln("[%s] VFD Resetting Fault/Jam", name.c_str()); + _vfd->resetFault(); +} + +void Plunger::_checkVfdForJam() +{ + unsigned long currentTimeMs = millis(); + if (debug_jam && currentTimeMs - _lastDiagnosticLogTimeMs > 5000) + { + uint16_t diagCurrentMa = 0; + bool diagReadSuccess = _vfd->getOutputCurrent(diagCurrentMa); + Log.infoln("[%s] --- DIAGNOSTIC LOG (debug_jam active) ---", name.c_str()); + Log.infoln("State: %s, FillState: %s, PostFlowState: %s", + _plungerStateToString(_currentState), + (_currentState == PlungerState::FILLING ? _fillStateToString(_currentFillState) : "N/A"), + (_currentState == PlungerState::POST_FLOW ? _postFlowStateToString(_currentPostFlowState) : "N/A")); + Log.infoln("VFD Running (reported): %d, VFD Fault (reported): %d", _vfd->isRunning(), _vfd->hasFault()); + Log.infoln("VFD Current Read Success: %d, Current: %u mA", diagReadSuccess, diagCurrentMa); + Log.infoln("JammedStartTime: %lu ms (ago: %lu ms if active)", _jammedStartTimeMs, (_jammedStartTimeMs > 0 ? currentTimeMs - _jammedStartTimeMs : 0)); + Log.infoln("JoystickHoldStartTime: %lu ms", _joystickHoldStartTimeMs); + Log.infoln("OperationStartTime: %lu ms, CurrentMaxOpTime: %lu ms", _operationStartTimeMs, _currentMaxOperationTimeMs); + Log.infoln("FillOperationStartTime: %lu ms", _fillOperationStartTimeMs); + _lastDiagnosticLogTimeMs = currentTimeMs; + } + + uint16_t vfdOutputCurrentMa = 0; + bool readSuccess = _vfd->getOutputCurrent(vfdOutputCurrentMa); + if (!readSuccess) + { + return; + } + + bool motorExpectedActive = false; + if (_currentState == PlungerState::HOMING_MANUAL || _currentState == PlungerState::HOMING_AUTO || + _currentState == PlungerState::PLUNGING_MANUAL || _currentState == PlungerState::PLUNGING_AUTO || + _currentState == PlungerState::RECORD || + (_currentState == PlungerState::POST_FLOW && _currentPostFlowState == PostFlowState::POST_FLOW_STARTING) || + (_currentState == PlungerState::FILLING && + (_currentFillState == FillState::PLUNGING || _currentFillState == FillState::HOMING))) + { + motorExpectedActive = true; + } + + if (_vfd->hasFault()) + { + Log.errorln("[%s] JAMMED (VFD FAULT)! VFD reports fault code %d. State: %s, FillState: %s", + name.c_str(), _vfd->getFaultCode(), + _plungerStateToString(_currentState), + _currentState == PlungerState::FILLING ? _fillStateToString(_currentFillState) : "N/A"); + _transitionToState(PlungerState::JAMMED); + return; + } + + if (!motorExpectedActive) + { + if (_jammedStartTimeMs != 0) + { + Log.verboseln("[%s] JAM CHECK: Motor not expected active. Jam timer RESET. State: %s, FillState: %s", + name.c_str(), _plungerStateToString(_currentState), + _currentState == PlungerState::FILLING ? _fillStateToString(_currentFillState) : "N/A"); + _jammedStartTimeMs = 0; + } + return; + } + + float torqueMultiplier = 1.0f; + uint16_t adjustedJamThresholdMa = _settings.currentJamThresholdMa; + + bool isPlungingState = (_currentState == PlungerState::PLUNGING_MANUAL || + _currentState == PlungerState::PLUNGING_AUTO || + _currentState == PlungerState::RECORD || + (_currentState == PlungerState::FILLING && _currentFillState == FillState::PLUNGING)); + + if (_currentState == PlungerState::POST_FLOW && _currentPostFlowState == PostFlowState::POST_FLOW_STARTING) + { + adjustedJamThresholdMa = _settings.currentPostFlowMa; + } + else if (isPlungingState) + { + torqueMultiplier = (100.0f - _currentTorquePotValue) / 100.0f; + adjustedJamThresholdMa = static_cast(_settings.currentJamThresholdMa * torqueMultiplier); + } + + if (vfdOutputCurrentMa >= adjustedJamThresholdMa) + { + if (_jammedStartTimeMs == 0) + { + _jammedStartTimeMs = millis(); + } + + unsigned long jamDurationTargetMs = 0; + if (_currentState == PlungerState::FILLING) + { + if (_currentFillState == FillState::PLUNGING) + jamDurationTargetMs = _settings.jammedDurationMs; + else if (_currentFillState == FillState::HOMING) + jamDurationTargetMs = _settings.jammedDurationHomingMs; + } + else if (_currentState == PlungerState::POST_FLOW && _currentPostFlowState == PostFlowState::POST_FLOW_STARTING) + { + jamDurationTargetMs = _settings.jammedDurationMs; + } + else if (_currentState == PlungerState::HOMING_MANUAL || _currentState == PlungerState::HOMING_AUTO) + { + jamDurationTargetMs = _settings.jammedDurationHomingMs; + } + else if (_currentState == PlungerState::PLUNGING_MANUAL || _currentState == PlungerState::PLUNGING_AUTO || _currentState == PlungerState::RECORD) + { + jamDurationTargetMs = _settings.jammedDurationMs; + } + + if (_jammedStartTimeMs > 0 && (millis() - _jammedStartTimeMs > _settings.maxUniversalJamTimeMs)) + { + Log.errorln("[%s] UNIVERSAL JAM TIMEOUT! Current %u mA for >%lums. State: %s, FillState: %s. JAMMED.", + name.c_str(), vfdOutputCurrentMa, millis() - _jammedStartTimeMs, + _plungerStateToString(_currentState), + _currentState == PlungerState::FILLING ? _fillStateToString(_currentFillState) : "N/A"); + _transitionToState(PlungerState::JAMMED); + return; + } + + if (jamDurationTargetMs > 0 && _jammedStartTimeMs > 0 && (millis() - _jammedStartTimeMs > jamDurationTargetMs)) + { + if (_currentState == PlungerState::PLUNGING_AUTO && _settings.enablePostFlow) + { + Log.infoln("[%s] Jam in PLUNGING_AUTO & postFlow enabled. -> POST_FLOW", name.c_str()); + _transitionToState(PlungerState::POST_FLOW); + } + else if (_currentState == PlungerState::FILLING) + { + _vfdStop(); + if (_currentFillState == FillState::PLUNGING) + { + Log.infoln("[%s] Fill: Jam during PLUNGING. -> PLUNGED. Wait %lu ms.", name.c_str(), _settings.fillPlungedWaitDurationMs); + _currentFillState = FillState::PLUNGED; + _fillSubStateTimer.once_ms(_settings.fillPlungedWaitDurationMs, &Plunger::_fillSubStateTimerRelay, this); + _jammedStartTimeMs = 0; + } + else if (_currentFillState == FillState::HOMING) + { + Log.infoln("[%s] Fill: Jam during HOMING. -> HOMED. Wait %lu ms.", name.c_str(), _settings.fillHomedWaitDurationMs); + _currentFillState = FillState::HOMED; + _fillSubStateTimer.once_ms(_settings.fillHomedWaitDurationMs, &Plunger::_fillSubStateTimerRelay, this); + _jammedStartTimeMs = 0; + } + } + else + { + _transitionToState(PlungerState::JAMMED); + } + } + else if (_jammedStartTimeMs > 0) + { + } + } + else // Current is NOT above threshold + { + if (_jammedStartTimeMs != 0) // If it *was* timing a jam + { + _jammedStartTimeMs = 0; + } + } +} + +short Plunger::loop() +{ + Component::loop(); + _updatePotValues(); + _checkVfdForJam(); + + if (debug_states) + { + unsigned long currentTimeMs = millis(); + if (currentTimeMs - _lastStateLogTimeMs >= 10000) + { + Log.infoln("[%s] --- STATE LOG DUMP (debug_states active) ---", name.c_str()); + Log.infoln(" CurrentTime: %lu ms", currentTimeMs); + Log.infoln(" State: %s (%d)", _plungerStateToString(_currentState), static_cast(_currentState)); + if (_currentState == PlungerState::FILLING) + { + Log.infoln(" FillState: %s (%d)", _fillStateToString(_currentFillState), static_cast(_currentFillState)); + } + if (_currentState == PlungerState::POST_FLOW) + { + Log.infoln(" PostFlowState: %s (%d)", _postFlowStateToString(_currentPostFlowState), static_cast(_currentPostFlowState)); + } + Log.infoln(" Timers (ms):"); + Log.infoln(" OperationStart: %d (Max: %d)", _operationStartTimeMs, _currentMaxOperationTimeMs); + Log.infoln(" JammedStart: %lu", _jammedStartTimeMs); + Log.infoln(" JoystickHoldStart: %lu", _joystickHoldStartTimeMs); + Log.infoln(" FillOperationStart: %lu", _fillOperationStartTimeMs); + Log.infoln(" PostFlowStart: %lu", _postFlowStartTimeMs); + Log.infoln(" RecordModeStart: %lu", _recordModeStartTimeMs); + Log.infoln(" LastStateChange: %lu", _lastStateChangeTimeMs); + _lastStateLogTimeMs = currentTimeMs; + Log.infoln("[%s] --- END STATE LOG DUMP ---", name.c_str()); + } + } + + if (_currentState == PlungerState::PLUNGING_MANUAL || _currentState == PlungerState::PLUNGING_AUTO) + { + // _calculatedPlungingSpeedHz is in 0.01Hz units, SAKO_VFD::setFrequency expects Hz + // _vfd->setFrequency(static_cast(_calculatedPlungingSpeedHz / 100.0f)); + } + // _vfd->setFrequency(static_cast(_currentSpeedPotValue / 100.0f)); + Joystick::E_POSITION currentJoystickDir = static_cast(_joystick->getValue()); + // Generic Max Operation Time Check (Safety Net) + if (_operationStartTimeMs > 0 && _currentMaxOperationTimeMs > 0) + { + bool isMonitoredState = false; + if (_currentState == PlungerState::HOMING_MANUAL || _currentState == PlungerState::HOMING_AUTO || + _currentState == PlungerState::PLUNGING_MANUAL || _currentState == PlungerState::PLUNGING_AUTO || + _currentState == PlungerState::RECORD || _currentState == PlungerState::REPLAY) + { + isMonitoredState = true; + } + else if (_currentState == PlungerState::FILLING && + (_currentFillState == FillState::PLUNGING || _currentFillState == FillState::HOMING)) + { + isMonitoredState = true; + } + else if (_currentState == PlungerState::POST_FLOW && _currentPostFlowState == PostFlowState::POST_FLOW_STARTING) + { + isMonitoredState = true; + } + + if (isMonitoredState && (millis() - _operationStartTimeMs > _currentMaxOperationTimeMs)) + { + Log.warningln("[%s] GENERIC MAX OPERATION TIME (%lu ms) EXCEEDED! State: %s, FillState: %s, PostFlowState: %s. Transitioning to JAMMED.", + name.c_str(), _currentMaxOperationTimeMs, + _plungerStateToString(_currentState), + _currentState == PlungerState::FILLING ? _fillStateToString(_currentFillState) : "N/A", + _currentState == PlungerState::POST_FLOW ? _postFlowStateToString(_currentPostFlowState) : "N/A"); + _transitionToState(PlungerState::JAMMED); + } + } + + switch (_currentState) + { + case PlungerState::IDLE: + _handleIdleState(); + break; + case PlungerState::HOMING_MANUAL: + _handleHomingManualState(); + break; + case PlungerState::HOMING_AUTO: + _handleHomingAutoState(); + break; + case PlungerState::PLUNGING_MANUAL: + _handlePlungingManualState(); + break; + case PlungerState::PLUNGING_AUTO: + _handlePlungingAutoState(); + break; + case PlungerState::STOPPING: + _handleStoppingState(); + break; + case PlungerState::JAMMED: + _handleJammedState(); + break; + case PlungerState::RESETTING_JAM: + _handleResettingJamState(); + break; + case PlungerState::RECORD: + _handleRecordState(); + break; + case PlungerState::REPLAY: + _handleReplayState(); + break; + case PlungerState::FILLING: // Re-added case + _handleFillingState(); + break; + case PlungerState::POST_FLOW: + _handlePostFlowState(); + break; + default: + Log.warningln("[%s] Unknown state: %d. Transitioning to IDLE.", name.c_str(), static_cast(_currentState)); + _transitionToState(PlungerState::IDLE); + break; + } + _lastJoystickDirection = currentJoystickDir; + return E_OK; +} + +short Plunger::info() +{ + Log.verboseln("--- Plunger Info (ID: %d, Name: %s) ---", id, name.c_str()); + + Log.verboseln("State: %d, LastJoy: %d, CurrentJoy: %d", + static_cast(_currentState), + static_cast(_lastJoystickDirection), + static_cast(_joystick->getValue())); + + Log.verboseln("SpeedPOT: %d, TorquePOT: %d", + _currentSpeedPotValue, _currentTorquePotValue); + uint16_t freq = 0; + uint16_t current = 0; + bool freqValid = _vfd->getFrequency(freq); + bool currentOk = _vfd->getOutputCurrent(current); + + Log.verboseln("VFD: Running=%s, Fault=%s, FreqSet=%d Hz, OutputCurrent=%d", + _vfd->isRunning() ? "YES" : "NO", + _vfd->hasFault() ? "YES" : "NO", + freqValid ? (static_cast(freq)) : -1, + currentOk ? static_cast(current) : -1); + return E_OK; +} + +short Plunger::debug() +{ + return info(); +} + +short Plunger::serial_register(Bridge *b) +{ + if (!b) + return E_INVALID_PARAMETER; + b->registerMemberFunction(id, this, C_STR("info"), (ComponentFnPtr)&Plunger::info); + b->registerMemberFunction(id, this, C_STR("plunge"), (ComponentFnPtr)&Plunger::cmd_plunge); + b->registerMemberFunction(id, this, C_STR("home"), (ComponentFnPtr)&Plunger::cmd_home); + b->registerMemberFunction(id, this, C_STR("stop"), (ComponentFnPtr)&Plunger::cmd_stop); + b->registerMemberFunction(id, this, C_STR("fill"), (ComponentFnPtr)&Plunger::cmd_fill); + b->registerMemberFunction(id, this, C_STR("enableAuto"), (ComponentFnPtr)&Plunger::cmd_enableAutoMode); + b->registerMemberFunction(id, this, C_STR("disableAuto"), (ComponentFnPtr)&Plunger::cmd_disableAutoMode); + b->registerMemberFunction(id, this, C_STR("saveSettings"), (ComponentFnPtr)&Plunger::cmd_save_settings); + b->registerMemberFunction(id, this, C_STR("loadDefaultSettings"), (ComponentFnPtr)&Plunger::cmd_load_default_settings); + b->registerMemberFunction(id, this, C_STR("replay"), (ComponentFnPtr)&Plunger::cmd_replay); + return E_OK; +} + +short Plunger::cmd_plunge() +{ + if (!_autoModeEnabled) + { + Log.warningln("[%s] cmd_plunge ignored. Auto mode is disabled.", name.c_str()); + return 1; + } + if (_currentState == PlungerState::IDLE) + { + Log.infoln("[%s] Initiating PLUNGING_AUTO from command.", name.c_str()); + _vfdStartForward(static_cast(_calculatedPlungingSpeedHz)); + _transitionToState(PlungerState::PLUNGING_AUTO); + return E_OK; + } + else + { + Log.warningln("[%s] cmd_plunge ignored. Current state is %d (not IDLE).", name.c_str(), static_cast(_currentState)); + return 1; + } +} + +short Plunger::cmd_home() +{ + if (!_autoModeEnabled) + { + Log.warningln("[%s] cmd_home ignored. Auto mode is disabled.", name.c_str()); + return 1; + } + if (_currentState == PlungerState::IDLE) + { + Log.infoln("[%s] Initiating HOMING_AUTO from command.", name.c_str()); + _vfdStartReverse(static_cast(_settings.speedSlowHz * 100.0f)); + _transitionToState(PlungerState::HOMING_AUTO); + return E_OK; + } + else + { + Log.warningln("[%s] cmd_home ignored. Current state is %d (not IDLE).", name.c_str(), static_cast(_currentState)); + return 1; + } +} + +short Plunger::cmd_stop() +{ + _transitionToState(PlungerState::STOPPING); + _vfdStop(); + _vfdResetJam(); + return E_OK; +} + +// Definition for cmd_fill +short Plunger::cmd_fill() +{ + if (_currentState != PlungerState::IDLE) + { + Log.warningln("[%s] cmd_fill ignored. Not IDLE. Current State: %s", + name.c_str(), _plungerStateToString(_currentState)); + return 1; + } + if (!_autoModeEnabled) + { + Log.warningln("[%s] cmd_fill ignored. Auto mode is disabled.", name.c_str()); + return 1; + } + Log.infoln("[%s] cmd_fill: Initiating FILLING sequence.", name.c_str()); + _currentFillState = FillState::PLUNGING; + _joystickReleasedSinceAutoStart = false; + _vfdStartForward(static_cast(_settings.speedFillPlungeHz * 100.0f)); + _operationStartTimeMs = millis(); + _currentMaxOperationTimeMs = _settings.defaultMaxOperationDurationMs; + _transitionToState(PlungerState::FILLING); + return E_OK; +} + +// New command implementation to save settings +short Plunger::cmd_save_settings() +{ + Log.infoln("[%s] cmd_save_settings: Attempting to save current settings to file...", name.c_str()); + if (_settings.save()) + { + Log.infoln("[%s] Settings successfully saved via command.", name.c_str()); + return E_OK; + } + else + { + Log.errorln("[%s] Failed to save settings via command.", name.c_str()); + return 1; // Or a more specific error code + } +} + +// Definition for reset +short Plunger::reset() +{ + Log.infoln("[%s] reset() called. Stopping VFD, clearing faults, and re-initializing.", name.c_str()); + _vfdStop(); + _vfdResetJam(); + return this->init(); +} + +// Definition for setAutoModeEnabled +void Plunger::setAutoModeEnabled(bool enabled) +{ + _autoModeEnabled = enabled; + Log.infoln("[%s] Auto mode %s.", name.c_str(), enabled ? "ENABLED" : "DISABLED"); +} + +// Definition for isAutoModeEnabled +bool Plunger::isAutoModeEnabled() const +{ + return _autoModeEnabled; +} + +// Definition for cmd_enableAutoMode +short Plunger::cmd_enableAutoMode() +{ + setAutoModeEnabled(true); + return E_OK; +} + +// Definition for cmd_disableAutoMode +short Plunger::cmd_disableAutoMode() +{ + setAutoModeEnabled(false); + return E_OK; +} + +// Method to serialize current settings to a JsonDocument +void Plunger::getSettingsJson(JsonDocument& doc) const { + _settings.toJson(doc); // Utilize the existing method in PlungerSettings +} + +// Method to update settings from a JsonObject and then save them +bool Plunger::updateSettingsFromJson(const JsonObject& json) { + if (!_settings.fromJson(json)) { // Utilize the existing method in PlungerSettings + Log.errorln("[%s] Failed to update settings from JSON in updateSettingsFromJson.", name.c_str()); + return false; + } + Log.infoln("[%s] Settings updated from JSON, attempting to save...", name.c_str()); + if (!_settings.save()) { // Utilize the existing save method in PlungerSettings + Log.errorln("[%s] Failed to save updated settings in updateSettingsFromJson.", name.c_str()); + // Decide if this is a hard failure for the method. + // For now, if fromJson succeeded but save failed, we might still return true + // as settings are updated in memory, but log the error. Or return false. + // Let's return false if save fails, to indicate the full operation wasn't successful. + return false; + } + Log.infoln("[%s] Settings successfully updated from JSON and saved.", name.c_str()); + // After successful update and save, re-initialize parts of Plunger that depend on settings + // For example, if _recordedPlungeDurationMs or _calculatedPlungingSpeedHz might change. + _recordedPlungeDurationMs = _settings.replayDurationMs; + _updatePotValues(); // This recalculates _calculatedPlungingSpeedHz + // Consider if a more full re-init or specific re-calculations are needed here. + // For now, updating these two common ones. + _settings.print(); + return true; +} + +short Plunger::cmd_load_default_settings() +{ + Log.infoln("[%s] cmd_load_default_settings: Attempting to load default settings...", name.c_str()); + if (loadDefaultSettings()) { // Uses default paths defined in Plunger.h declaration + Log.infoln("[%s] Default settings loaded and applied successfully via command.", name.c_str()); + return E_OK; + } else { + Log.errorln("[%s] Failed to load or apply default settings via command.", name.c_str()); + return 1; // Or a more specific error code + } +} + +bool Plunger::loadDefaultSettings(const char* defaultPath, const char* operationalPath) { + Log.infoln("[%s] Attempting to load settings from default path: %s", name.c_str(), defaultPath); + PlungerSettings tempSettings = _settings; // Create a copy to attempt loading into + + if (!tempSettings.load(defaultPath)) { + Log.errorln("[%s] Failed to load settings from default file: %s", name.c_str(), defaultPath); + return false; + } + Log.infoln("[%s] Successfully loaded settings from %s. Now applying and saving to %s.", name.c_str(), defaultPath, operationalPath); + + _settings = tempSettings; // Apply loaded settings to the main _settings object + + if (!_settings.save(operationalPath)) { + Log.errorln("[%s] Failed to save the loaded default settings to operational path: %s", name.c_str(), operationalPath); + // Depending on requirements, this might still be considered a partial success if memory update is enough. + // For now, let's say if save fails, the operation wasn't fully successful. + return false; + } + + // Re-apply any settings-dependent internal states + _recordedPlungeDurationMs = _settings.replayDurationMs; + _updatePotValues(); // This recalculates _calculatedPlungingSpeedHz + _settings.print(); // Log the newly applied settings + + Log.infoln("[%s] Default settings loaded from %s, applied, and saved to %s.", name.c_str(), defaultPath, operationalPath); + return true; +} + +short Plunger::cmd_replay() +{ + if (_currentState != PlungerState::IDLE) + { + Log.warningln("[%s] cmd_replay ignored. Not IDLE. Current State: %s", name.c_str(), _plungerStateToString(_currentState)); + return 1; + } + if (_recordedPlungeDurationMs <= 50) { // Same check as in _handleReplayState + Log.warningln("[%s] cmd_replay ignored. Invalid or zero replay duration (%lu ms).", name.c_str(), _recordedPlungeDurationMs); + return 1; + } + if (!_autoModeEnabled) // Consider if replay should be subject to autoModeEnabled + { + Log.warningln("[%s] cmd_replay ignored. Auto mode is disabled.", name.c_str()); + return 1; + } // Depending on requirements, you might allow replay even if auto mode is off. + + Log.infoln("[%s] Initiating REPLAY from command.", name.c_str()); + _transitionToState(PlungerState::REPLAY); + // _handleReplayState will start the VFD and set timers. + return E_OK; +} diff --git a/src/components/Plunger.h b/src/components/Plunger.h new file mode 100644 index 00000000..e1408fb9 --- /dev/null +++ b/src/components/Plunger.h @@ -0,0 +1,247 @@ +#ifndef PLUNGER_H +#define PLUNGER_H + +#include "config.h" +#include +#include +#include +#include +#include "components/SAKO_VFD.h" +#include "components/Joystick.h" +#include "components/POT.h" +#include "enums.h" +#include "modbus/ModbusTCP.h" +#include "PlungerSettings.h" + +// Forward declaration for the debug flag +extern const bool debug_states; + +// Component Constants +#define PLUNGER_COMPONENT_ID 760 +#define PLUNGER_COMPONENT_NAME "Plunger" + +// Speed Presets (in 0.01 Hz units for SAKO_VFD) +#define PLUNGER_SPEED_SLOW_HZ 20 // 10.00 Hz +#define PLUNGER_SPEED_MEDIUM_HZ 25 // 25.00 Hz +#define PLUNGER_SPEED_FAST_HZ 50 +#define PLUNGER_SPEED_MAX_HZ 75 + +// Post-flow settings +#define PLUNGER_POST_FLOW_DURATION_MS 1000 // Duration of active post-flow (pressing) +#define PLUNGER_POST_FLOW_SPEED_HZ 5 // Speed during post-flow pressing +#define PLUNGER_CURRENT_POST_FLOW_MA 1200 // Current at or above this value indicates post-flow jam +#define PLUNGER_POST_FLOW_STOPPING_WAIT_MS 1500 // Wait time after initial stop before starting post-flow press +#define PLUNGER_POST_FLOW_COMPLETE_WAIT_MS 1500 // Wait time after post-flow press before returning to IDLE +#define PLUNGER_DEFAULT_ENABLE_POST_FLOW true // Added for clarity if PlungerSettings uses it + +// New constants for Filling +#define PLUNGER_FILL_JOYSTICK_HOLD_DURATION_MS 2500 // Time to hold joystick LEFT to start fill +#define PLUNGER_SPEED_FILL_PLUNGE_HZ PLUNGER_SPEED_MEDIUM_HZ // Speed for plunging phase of filling +#define PLUNGER_SPEED_FILL_HOME_HZ PLUNGER_SPEED_SLOW_HZ // Speed for homing phase of filling +#define PLUNGER_FILL_PLUNGED_WAIT_DURATION_MS 1000 // Wait duration after plunging +#define PLUNGER_FILL_HOMED_WAIT_DURATION_MS 1500 // Wait duration after homing + +// Speed POT Configuration for Plunging (multiplier for MEDIUM speed) +// POT value 0-100. Example: 0 maps to 0.5x, 50 maps to 1.0x, 100 maps to 1.5x MEDIUM speed. + +// Fixed Current Thresholds (mA) +#define PLUNGER_CURRENT_IDLE_LOWER_MA 300 +#define PLUNGER_CURRENT_IDLE_UPPER_MA 500 +#define PLUNGER_CURRENT_PLUNGING_LOWER_MA 500 +#define PLUNGER_CURRENT_PLUNGING_UPPER_MA 700 +#define PLUNGER_CURRENT_JAM_THRESHOLD_MA 700 // Current at or above this value indicates a jam + + +#define PLUNGER_JAMMED_DURATION_HOMING_MS 10 +#define PLUNGER_JAMMED_DURATION_MS 1400 +#define PLUNGER_VFD_READ_INTERVAL_MS 200 + +// #define ENABLE_AUTO 1 // Set to 0 to disable joystick-activated auto homing/plunging -- REMOVED + +#define PLUNGER_AUTO_MODE_HOLD_DURATION_MS 2000 // Time joystick must be held for auto mode +#define PLUNGER_MAX_UNIVERSAL_JAM_TIME_MS 5000 // Max time current can be high before a universal jam + +// New constants for Record/Replay +#define PLUNGER_RECORD_HOLD_DURATION_MS 2000 // Min time joystick must be held RIGHT to enter RECORD mode +#define PLUNGER_MAX_RECORD_DURATION_MS 20000 // Max duration for the plunging phase of recording +#define PLUNGER_DEFAULT_REPLAY_DURATION_MS 4500 // Default duration for replay if not recorded/set + +// New constant for default max operation time +#define PLUNGER_DEFAULT_MAX_OPERATION_DURATION_MS 60*1000*1 // 1 minute + +// Modbus Configuration +#define PLUNGER_MB_BASE_ADDRESS COMPONENT_KEY_PLUNGER // Using component ID as base +#define PLUNGER_MB_STATE_OFFSET 0 +#define PLUNGER_MB_COMMAND_OFFSET 1 +#define PLUNGER_MB_BLOCK_COUNT 2 // Will need to update if CMD_FILL Modbus register is separate, for now assume it uses existing command offset +#define PLUNGER_MAX_RUN_TIME_MEDIUM_SPEED_MS 15000 // Max runtime at medium speed + +enum class E_PlungerCommand : short { + NO_COMMAND = 0, + CMD_HOME = 1, + CMD_PLUNGE = 2, + CMD_STOP = 3, + CMD_INFO = 4, + CMD_FILL = 5, + CMD_REPLAY = 6 +}; + +// Plunger States +enum class PlungerState : uint8_t { + IDLE, + HOMING_MANUAL, // Joystick held DOWN, VFD reversing, monitoring for auto-mode hold time + HOMING_AUTO, // Auto-homing after joystick hold, VFD reversing + PLUNGING_MANUAL, // Joystick held UP, VFD forwarding, monitoring for auto-mode hold time + PLUNGING_AUTO, // Auto-plunging after joystick hold, VFD forwarding + STOPPING, // Transition state to stop VFD + JAMMED, + RESETTING_JAM, // State to handle reset after jam + RECORD, // New state for recording plunge duration + REPLAY, // New state for replaying recorded plunge + FILLING, + POST_FLOW, +}; + +enum class FillState : uint8_t +{ + NONE, + PLUNGING, // Actively plunging down + PLUNGED, // Plunge event (jam/max_duration) occurred, waiting for min plunge time + HOMING, // Actively homing up + HOMED // Homing complete +}; + +enum class PostFlowState : uint8_t +{ + NONE, + POST_FLOW_STOPPING, // Stopping VFD, and wait at least 1500ms + POST_FLOW_STARTING, // Starting VFD, set post-flow speed, and run for post-flow duration + POST_FLOW_COMPLETE // Stop VFD, and wait before resuming normal operation (e.g. IDLE) +}; + +class Plunger : public Component { +public: + Plunger(Component* owner, SAKO_VFD* vfd, Joystick* joystick, POT* speedPot, POT* torquePot); + ~Plunger() override = default; + + short setup() override; + short loop() override; + short info() override; + short debug() override; + short serial_register(Bridge* b) override; + short init(); + short reset(); + + // Modbus TCP Interface + ModbusBlockView* mb_tcp_blocks() const override; + void mb_tcp_register(ModbusTCP* mgr) const override; + short mb_tcp_read(MB_Registers* reg) override; + short mb_tcp_write(MB_Registers* reg, short networkValue) override; + // Public commands for serial/external control + short cmd_plunge(); + short cmd_home(); + short cmd_stop(); + short cmd_fill(); + short cmd_save_settings(); + short cmd_load_default_settings(); + short cmd_replay(); + // Auto mode control + void setAutoModeEnabled(bool enabled); + bool isAutoModeEnabled() const; + short cmd_enableAutoMode(); + short cmd_disableAutoMode(); + + // Persistence related methods (now for REST exposure) + void getSettingsJson(JsonDocument& doc) const; + bool updateSettingsFromJson(const JsonObject& json); + bool loadDefaultSettings(const char* defaultPath = "/plunger_default.json", const char* operationalPath = "/plunger.json"); + +private: + SAKO_VFD* _vfd; + Joystick* _joystick; + POT* _speedPot; + POT* _torquePot; + PlungerState _currentState; + FillState _currentFillState; // Re-added + PostFlowState _currentPostFlowState; // For managing sub-states of POST_FLOW + Joystick::E_POSITION _lastJoystickDirection; + uint16_t _currentSpeedPotValue; // 0-100 + uint16_t _currentTorquePotValue; // 0-100, for torque/jam sensitivity + float _calculatedPlungingSpeedHz; // Calculated speed for plunging (0.01Hz units) + + unsigned long _lastStateChangeTimeMs; + unsigned long _jammedStartTimeMs; + unsigned long _lastVfdReadTimeMs; + unsigned long _joystickHoldStartTimeMs; // Timer for joystick hold duration + short _modbusCommandRegisterValue; // Holds the current value of the Modbus command register + unsigned long _operationStartTimeMs; // Start time of current plunge/home operation (used for fill plunge phase) + unsigned long _currentMaxOperationTimeMs; // Calculated max duration for current operation (used for fill plunge phase) + unsigned long _lastDiagnosticLogTimeMs; // Timer for the 5-second diagnostic log in _checkVfdForJam + unsigned long _lastImmediateStopCheckTimeMs; // Timer for the 1-second immediate high-current stop check + bool _joystickReleasedSinceAutoStart; // True if joystick released to center after auto mode started + bool _autoModeEnabled; // True if joystick/command auto modes are enabled + + // Record and Replay members + Ticker _joystickRecordHoldTimer; + Ticker _replayPlungeTimer; + Ticker _joystickFillHoldTimer; // Timer for joystick LEFT hold to start filling + unsigned long _recordedPlungeDurationMs; + unsigned long _recordModeStartTimeMs; // To time the actual plunge during recording + + // Filling members + Ticker _fillSubStateTimer; // Timer for fill sub-state durations + unsigned long _fillOperationStartTimeMs; // Start time of the overall fill operation + unsigned long _postFlowStartTimeMs; // Start time of the active post-flow pressing phase + + // Post-flow sub-state timer + Ticker _postFlowSubStateTimer; + + // Settings + PlungerSettings _settings; + + // Timer for state logging + unsigned long _lastStateLogTimeMs; + + // Helper methods + void _handleIdleState(); + void _handleHomingManualState(); + void _handleHomingAutoState(); + void _handlePlungingManualState(); + void _handlePlungingAutoState(); + void _handleStoppingState(); + void _handleJammedState(); + void _handleResettingJamState(); + void _handleRecordState(); + void _handleReplayState(); + void _handleFillingState(); + void _handlePostFlowState(); // Declaration for the new state handler + // Static relay callbacks for Ticker + static void _joystickRecordHoldTimerRelay(Plunger* pThis); + static void _replayPlungeTimerRelay(Plunger* pThis); + static void _fillSubStateTimerRelay(Plunger* pThis); // Added for fill timer + static void _joystickFillHoldTimerRelay(Plunger* pThis); // Relay for fill joystick hold + static void _postFlowSubStateTimerRelay(Plunger* pThis); // Relay for post-flow sub-state timer + + // Actual timer event handlers + void _onJoystickRecordHoldTimeout(); + void _onReplayPlungeTimeout(); + void _onFillSubStateTimeout(); // Added for fill timer + void _onJoystickFillHoldTimeout(); // Handler for fill joystick hold + void _onPostFlowSubStateTimeout(); // Handler for post-flow sub-state timer + + void _updatePotValues(); + void _checkVfdForJam(); + void _transitionToState(PlungerState newState); + + // VFD interaction wrappers + void _vfdStartForward(uint16_t frequencyCentiHz); + void _vfdStartReverse(uint16_t frequencyCentiHz); + void _vfdStop(); + void _vfdResetJam(); +}; + +const char *_plungerStateToString(PlungerState state); +const char *_fillStateToString(FillState state); // Ensure this declaration is present +const char *_postFlowStateToString(PostFlowState state); // Declaration for PostFlowState to string + +#endif // PLUNGER_H \ No newline at end of file diff --git a/src/components/PlungerModbus.cpp b/src/components/PlungerModbus.cpp new file mode 100644 index 00000000..dc8492ac --- /dev/null +++ b/src/components/PlungerModbus.cpp @@ -0,0 +1,117 @@ +#include "Plunger.h" +#include + +ModbusBlockView *Plunger::mb_tcp_blocks() const +{ + static MB_Registers blocks[PLUNGER_MB_BLOCK_COUNT] = { + {static_cast(PLUNGER_MB_BASE_ADDRESS + PLUNGER_MB_STATE_OFFSET), + 1, + E_FN_CODE::FN_READ_HOLD_REGISTER, + MB_ACCESS_READ_ONLY, + static_cast(id), + PLUNGER_MB_STATE_OFFSET, + "Plunger State", + PLUNGER_COMPONENT_NAME}, + {static_cast(PLUNGER_MB_BASE_ADDRESS + PLUNGER_MB_COMMAND_OFFSET), + 1, + E_FN_CODE::FN_READ_HOLD_REGISTER, + MB_ACCESS_READ_WRITE, + static_cast(id), + PLUNGER_MB_COMMAND_OFFSET, + "Plunger Command (0:None,1:Home,2:Plunge,3:Stop,4:Info,5:Fill)", + PLUNGER_COMPONENT_NAME}}; + static ModbusBlockView blockView = {blocks, PLUNGER_MB_BLOCK_COUNT}; + return &blockView; +} + +void Plunger::mb_tcp_register(ModbusTCP *mgr) const +{ + if (!mgr) + return; + ModbusBlockView *blocksView = mb_tcp_blocks(); + Component *thiz = const_cast(this); + for (int i = 0; i < blocksView->count; ++i) + { + mgr->registerModbus(thiz, blocksView->data[i]); + } +} + +short Plunger::mb_tcp_read(MB_Registers *reg) +{ + if (!reg) + return MODBUS_ERROR_ILLEGAL_DATA_ADDRESS; + + uint16_t address = reg->startAddress; + + if (address == (PLUNGER_MB_BASE_ADDRESS + PLUNGER_MB_STATE_OFFSET)) + { + return static_cast(_currentState); + } + else if (address == (PLUNGER_MB_BASE_ADDRESS + PLUNGER_MB_COMMAND_OFFSET)) + { + return _modbusCommandRegisterValue; + } + return MODBUS_ERROR_ILLEGAL_DATA_ADDRESS; +} + +short Plunger::mb_tcp_write(MB_Registers *reg, short networkValue) +{ + if (!reg) + return MODBUS_ERROR_ILLEGAL_DATA_ADDRESS; + + uint16_t address = reg->startAddress; + + if (address == (PLUNGER_MB_BASE_ADDRESS + PLUNGER_MB_COMMAND_OFFSET)) + { + E_PlungerCommand cmd = static_cast(networkValue); + _modbusCommandRegisterValue = networkValue; + + short result = E_OK; + switch (cmd) + { + case E_PlungerCommand::CMD_HOME: + result = this->cmd_home(); + break; + case E_PlungerCommand::CMD_PLUNGE: + result = this->cmd_plunge(); + break; + case E_PlungerCommand::CMD_STOP: + result = this->cmd_stop(); + break; + case E_PlungerCommand::CMD_INFO: + result = this->info(); + break; + case E_PlungerCommand::CMD_FILL: + result = this->cmd_fill(); + break; + case E_PlungerCommand::CMD_REPLAY: + result = this->cmd_replay(); + break; + case E_PlungerCommand::NO_COMMAND: + Log.verboseln("[%s] Modbus NO_COMMAND received.", name.c_str()); + break; + default: + Log.warningln("[%s] Unknown Modbus command received: %d", name.c_str(), networkValue); + result = MODBUS_ERROR_ILLEGAL_DATA_VALUE; + break; + } + + if (cmd != E_PlungerCommand::NO_COMMAND && result == E_OK) + { + _modbusCommandRegisterValue = static_cast(E_PlungerCommand::NO_COMMAND); + } + else if (result != E_OK && result != MODBUS_ERROR_ILLEGAL_DATA_VALUE) + { + _modbusCommandRegisterValue = static_cast(E_PlungerCommand::NO_COMMAND); + } + + return (result == E_OK || result == 1) ? E_OK : result; + } + else if (address == (PLUNGER_MB_BASE_ADDRESS + PLUNGER_MB_STATE_OFFSET)) + { + Log.warningln("[%s] mb_tcp_write: Attempt to write to read-only State register %d", name.c_str(), address); + return MODBUS_ERROR_ILLEGAL_FUNCTION; + } + + return MODBUS_ERROR_ILLEGAL_DATA_ADDRESS; +} diff --git a/src/components/PlungerSettings.cpp b/src/components/PlungerSettings.cpp new file mode 100644 index 00000000..245fc3b0 --- /dev/null +++ b/src/components/PlungerSettings.cpp @@ -0,0 +1,213 @@ +#include "PlungerSettings.h" +#include "Plunger.h" +#include +#include +#include + +PlungerSettings::PlungerSettings() : + speedSlowHz(PLUNGER_SPEED_SLOW_HZ), + speedMediumHz(PLUNGER_SPEED_MEDIUM_HZ), + speedFastHz(PLUNGER_SPEED_FAST_HZ), + speedFillPlungeHz(PLUNGER_SPEED_FILL_PLUNGE_HZ), + speedFillHomeHz(PLUNGER_SPEED_FILL_HOME_HZ), + currentJamThresholdMa(PLUNGER_CURRENT_JAM_THRESHOLD_MA), + jammedDurationHomingMs(PLUNGER_JAMMED_DURATION_HOMING_MS), + jammedDurationMs(PLUNGER_JAMMED_DURATION_MS), + autoModeHoldDurationMs(PLUNGER_AUTO_MODE_HOLD_DURATION_MS), + maxUniversalJamTimeMs(PLUNGER_MAX_UNIVERSAL_JAM_TIME_MS), + fillJoystickHoldDurationMs(PLUNGER_FILL_JOYSTICK_HOLD_DURATION_MS), + fillPlungedWaitDurationMs(PLUNGER_FILL_PLUNGED_WAIT_DURATION_MS), + fillHomedWaitDurationMs(PLUNGER_FILL_HOMED_WAIT_DURATION_MS), + recordHoldDurationMs(PLUNGER_RECORD_HOLD_DURATION_MS), + maxRecordDurationMs(PLUNGER_MAX_RECORD_DURATION_MS), + replayDurationMs(PLUNGER_DEFAULT_REPLAY_DURATION_MS), + enablePostFlow(PLUNGER_DEFAULT_ENABLE_POST_FLOW), + postFlowDurationMs(PLUNGER_POST_FLOW_DURATION_MS), + postFlowSpeedHz(PLUNGER_POST_FLOW_SPEED_HZ), + currentPostFlowMa(PLUNGER_CURRENT_POST_FLOW_MA), + postFlowStoppingWaitMs(PLUNGER_POST_FLOW_STOPPING_WAIT_MS), + postFlowCompleteWaitMs(PLUNGER_POST_FLOW_COMPLETE_WAIT_MS), + defaultMaxOperationDurationMs(PLUNGER_DEFAULT_MAX_OPERATION_DURATION_MS) +{ + +} + +void PlungerSettings::toJson(JsonDocument& doc) const { + JsonObject obj = doc.to(); // Or doc.as() if doc is already object type + + obj["speedSlowHz"] = speedSlowHz; + obj["speedMediumHz"] = speedMediumHz; + obj["speedFastHz"] = speedFastHz; + obj["speedFillPlungeHz"] = speedFillPlungeHz; + obj["speedFillHomeHz"] = speedFillHomeHz; + obj["currentJamThresholdMa"] = currentJamThresholdMa; + obj["jammedDurationHomingMs"] = jammedDurationHomingMs; + obj["jammedDurationMs"] = jammedDurationMs; + obj["autoModeHoldDurationMs"] = autoModeHoldDurationMs; + obj["maxUniversalJamTimeMs"] = maxUniversalJamTimeMs; + obj["fillJoystickHoldDurationMs"] = fillJoystickHoldDurationMs; + obj["fillPlungedWaitDurationMs"] = fillPlungedWaitDurationMs; + obj["fillHomedWaitDurationMs"] = fillHomedWaitDurationMs; + obj["recordHoldDurationMs"] = recordHoldDurationMs; + obj["maxRecordDurationMs"] = maxRecordDurationMs; + obj["replayDurationMs"] = replayDurationMs; + obj["enablePostFlow"] = enablePostFlow; + obj["postFlowDurationMs"] = postFlowDurationMs; + obj["postFlowSpeedHz"] = postFlowSpeedHz; + obj["currentPostFlowMa"] = currentPostFlowMa; + obj["postFlowStoppingWaitMs"] = postFlowStoppingWaitMs; + obj["postFlowCompleteWaitMs"] = postFlowCompleteWaitMs; + obj["defaultMaxOperationDurationMs"] = defaultMaxOperationDurationMs; +} + +// --- Helper functions for fromJson --- +// (Defined static or as private members if preferred; static here for simplicity) +static void _parseJsonField(const JsonObject& json, const char* key, uint16_t& targetValue, const char* fieldName) { + if (json.containsKey(key)) { + if (json[key].is()) { + targetValue = json[key].as(); + // Log.verboseln("[PlungerSettings] Parsed '%s': %u", fieldName, targetValue); // Optional verbose log + } else { + Log.warningln("[PlungerSettings] WARN: '%s' in JSON is not uint16_t. Using default: %u", fieldName, targetValue); + } + } // If key doesn't exist, targetValue retains its default constructor-initialized value. +} + +static void _parseJsonField(const JsonObject& json, const char* key, uint32_t& targetValue, const char* fieldName) { + if (json.containsKey(key)) { + if (json[key].is()) { + targetValue = json[key].as(); + // Log.verboseln("[PlungerSettings] Parsed '%s': %lu", fieldName, targetValue); // Optional verbose log + } else { + Log.warningln("[PlungerSettings] WARN: '%s' in JSON is not uint32_t. Using default: %lu", fieldName, targetValue); + } + } +} + +static void _parseJsonField(const JsonObject& json, const char* key, bool& targetValue, const char* fieldName) { + if (json.containsKey(key)) { + if (json[key].is()) { + targetValue = json[key].as(); + // Log.verboseln("[PlungerSettings] Parsed '%s': %s", fieldName, targetValue ? "true" : "false"); // Optional verbose log + } else { + Log.warningln("[PlungerSettings] WARN: '%s' in JSON is not bool. Using default: %s", fieldName, targetValue ? "true" : "false"); + } + } +} + +bool PlungerSettings::fromJson(const JsonObject& json) { + if (json.isNull()) { + Log.warningln("[PlungerSettings] fromJson: Provided JSON object is null. Using defaults."); + return false; + } + _parseJsonField(json, "speedSlowHz", speedSlowHz, "speedSlowHz"); + _parseJsonField(json, "speedMediumHz", speedMediumHz, "speedMediumHz"); + _parseJsonField(json, "speedFastHz", speedFastHz, "speedFastHz"); + _parseJsonField(json, "speedFillPlungeHz", speedFillPlungeHz, "speedFillPlungeHz"); + _parseJsonField(json, "speedFillHomeHz", speedFillHomeHz, "speedFillHomeHz"); + _parseJsonField(json, "currentJamThresholdMa", currentJamThresholdMa, "currentJamThresholdMa"); + _parseJsonField(json, "jammedDurationHomingMs", jammedDurationHomingMs, "jammedDurationHomingMs"); + _parseJsonField(json, "jammedDurationMs", jammedDurationMs, "jammedDurationMs"); + _parseJsonField(json, "autoModeHoldDurationMs", autoModeHoldDurationMs, "autoModeHoldDurationMs"); + _parseJsonField(json, "maxUniversalJamTimeMs", maxUniversalJamTimeMs, "maxUniversalJamTimeMs"); + _parseJsonField(json, "fillJoystickHoldDurationMs", fillJoystickHoldDurationMs, "fillJoystickHoldDurationMs"); + _parseJsonField(json, "fillPlungedWaitDurationMs", fillPlungedWaitDurationMs, "fillPlungedWaitDurationMs"); + _parseJsonField(json, "fillHomedWaitDurationMs", fillHomedWaitDurationMs, "fillHomedWaitDurationMs"); + _parseJsonField(json, "recordHoldDurationMs", recordHoldDurationMs, "recordHoldDurationMs"); + _parseJsonField(json, "maxRecordDurationMs", maxRecordDurationMs, "maxRecordDurationMs"); + _parseJsonField(json, "replayDurationMs", replayDurationMs, "replayDurationMs"); + _parseJsonField(json, "postFlowDurationMs", postFlowDurationMs, "postFlowDurationMs"); + _parseJsonField(json, "postFlowStoppingWaitMs", postFlowStoppingWaitMs, "postFlowStoppingWaitMs"); + _parseJsonField(json, "postFlowCompleteWaitMs", postFlowCompleteWaitMs, "postFlowCompleteWaitMs"); + _parseJsonField(json, "defaultMaxOperationDurationMs", defaultMaxOperationDurationMs, "defaultMaxOperationDurationMs"); + + _parseJsonField(json, "enablePostFlow", enablePostFlow, "enablePostFlow"); + + _parseJsonField(json, "postFlowSpeedHz", postFlowSpeedHz, "postFlowSpeedHz"); + _parseJsonField(json, "currentPostFlowMa", currentPostFlowMa, "currentPostFlowMa"); + + Log.infoln("[PlungerSettings] Settings parsed from JSON (check warnings above for issues). Call print() to see final values."); + return true; +} + +bool PlungerSettings::load(const char* path) { + if (!LittleFS.begin()) { + Log.errorln("[PlungerSettings] Failed to initialize LittleFS for load."); + return false; + } + + File configFile = LittleFS.open(path, "r"); + if (!configFile) { + Log.warningln("[PlungerSettings] Settings file not found: %s. Using current (default) settings.", path); + // Optionally, save current (default) settings to create the file: + // save(path); + return false; // Indicate that loading from file did not happen, defaults remain. + } + + JsonDocument doc; + DeserializationError error = deserializeJson(doc, configFile); + configFile.close(); + + if (error) { + Log.errorln("[PlungerSettings] Failed to deserialize settings file %s: %s", path, error.c_str()); + return false; + } + + Log.infoln("[PlungerSettings] Successfully deserialized %s.", path); + return fromJson(doc.as()); +} + +bool PlungerSettings::save(const char* path) const { + if (!LittleFS.begin()) { + Log.errorln("[PlungerSettings] Failed to initialize LittleFS for save."); + return false; + } + + JsonDocument doc; + toJson(doc); // Populate the document with current settings + + File configFile = LittleFS.open(path, "w"); + if (!configFile) { + Log.errorln("[PlungerSettings] Failed to open settings file for writing: %s", path); + return false; + } + + size_t bytesWritten = serializeJson(doc, configFile); + configFile.close(); + + if (bytesWritten == 0) { + Log.errorln("[PlungerSettings] Failed to write settings to file: %s", path); + return false; + } + + Log.infoln("[PlungerSettings] Settings successfully saved to %s (%u bytes).", path, bytesWritten); + return true; +} + +void PlungerSettings::print() const { + Log.infoln("--- PlungerSettings Values ---"); + Log.infoln(" speedSlowHz: %u", speedSlowHz); + Log.infoln(" speedMediumHz: %u", speedMediumHz); + Log.infoln(" speedFastHz: %u", speedFastHz); + Log.infoln(" speedFillPlungeHz: %u", speedFillPlungeHz); + Log.infoln(" speedFillHomeHz: %u", speedFillHomeHz); + Log.infoln(" currentJamThresholdMa: %u", currentJamThresholdMa); + Log.infoln(" jammedDurationHomingMs: %lu", jammedDurationHomingMs); + Log.infoln(" jammedDurationMs: %lu", jammedDurationMs); + Log.infoln(" autoModeHoldDurationMs: %lu", autoModeHoldDurationMs); + Log.infoln(" maxUniversalJamTimeMs: %lu", maxUniversalJamTimeMs); + Log.infoln(" fillJoystickHoldDurationMs: %lu", fillJoystickHoldDurationMs); + Log.infoln(" fillPlungedWaitDurationMs: %lu", fillPlungedWaitDurationMs); + Log.infoln(" fillHomedWaitDurationMs: %lu", fillHomedWaitDurationMs); + Log.infoln(" recordHoldDurationMs: %lu", recordHoldDurationMs); + Log.infoln(" maxRecordDurationMs: %lu", maxRecordDurationMs); + Log.infoln(" replayDurationMs: %lu", replayDurationMs); + Log.infoln(" enablePostFlow: %s", enablePostFlow ? "Yes" : "No"); + Log.infoln(" postFlowDurationMs: %lu", postFlowDurationMs); + Log.infoln(" postFlowSpeedHz: %u", postFlowSpeedHz); + Log.infoln(" currentPostFlowMa: %u", currentPostFlowMa); + Log.infoln(" postFlowStoppingWaitMs: %lu", postFlowStoppingWaitMs); + Log.infoln(" postFlowCompleteWaitMs: %lu", postFlowCompleteWaitMs); + Log.infoln(" defaultMaxOperationDurationMs: %lu", defaultMaxOperationDurationMs); + Log.infoln("--- End PlungerSettings Values ---"); +} \ No newline at end of file diff --git a/src/components/PlungerSettings.h b/src/components/PlungerSettings.h new file mode 100644 index 00000000..dd129399 --- /dev/null +++ b/src/components/PlungerSettings.h @@ -0,0 +1,107 @@ +#ifndef PLUNGER_SETTINGS_H +#define PLUNGER_SETTINGS_H + +#include // For uint16_t, uint32_t +#include // For bool type +#include // For JSON operations +#include // For file operations + +// No include "Plunger.h" here to avoid circular dependencies for this file's primary role. +// The .cpp file for PlungerSettings will include Plunger.h for constants if needed for default constructor. + +struct PlungerSettings { + uint16_t speedSlowHz; + uint16_t speedMediumHz; + uint16_t speedFastHz; + uint16_t speedFillPlungeHz; + uint16_t speedFillHomeHz; + + uint16_t currentJamThresholdMa; + + uint32_t jammedDurationHomingMs; + uint32_t jammedDurationMs; + uint32_t autoModeHoldDurationMs; + uint32_t maxUniversalJamTimeMs; + + uint32_t fillJoystickHoldDurationMs; + uint32_t fillPlungedWaitDurationMs; + uint32_t fillHomedWaitDurationMs; + + uint32_t recordHoldDurationMs; + uint32_t maxRecordDurationMs; + uint32_t replayDurationMs; + + bool enablePostFlow; + uint32_t postFlowDurationMs; + uint16_t postFlowSpeedHz; + uint16_t currentPostFlowMa; + uint32_t postFlowStoppingWaitMs; + uint32_t postFlowCompleteWaitMs; + + uint32_t defaultMaxOperationDurationMs; + + // Default constructor - Declaration only, defined in .cpp + PlungerSettings(); + + // Constructor to initialize with default values from Plunger.h constants + PlungerSettings( + uint16_t defSpeedSlowHz, + uint16_t defSpeedMediumHz, + uint16_t defSpeedFastHz, + uint16_t defSpeedFillPlungeHz, + uint16_t defSpeedFillHomeHz, + uint16_t defCurrentJamThresholdMa, + uint32_t defJammedDurationHomingMs, + uint32_t defJammedDurationMs, + uint32_t defAutoModeHoldDurationMs, + uint32_t defMaxUniversalJamTimeMs, + uint32_t defFillJoystickHoldDurationMs, + uint32_t defFillPlungedWaitDurationMs, + uint32_t defFillHomedWaitDurationMs, + uint32_t defRecordHoldDurationMs, + uint32_t defMaxRecordDurationMs, + uint32_t defReplayDurationMs, + bool defEnablePostFlow, + uint32_t defPostFlowDurationMs, + uint16_t defPostFlowSpeedHz, + uint16_t defCurrentPostFlowMa, + uint32_t defPostFlowStoppingWaitMs, + uint32_t defPostFlowCompleteWaitMs, + uint32_t defDefaultMaxOperationDurationMs + ) : + speedSlowHz(defSpeedSlowHz), + speedMediumHz(defSpeedMediumHz), + speedFastHz(defSpeedFastHz), + speedFillPlungeHz(defSpeedFillPlungeHz), + speedFillHomeHz(defSpeedFillHomeHz), + currentJamThresholdMa(defCurrentJamThresholdMa), + jammedDurationHomingMs(defJammedDurationHomingMs), + jammedDurationMs(defJammedDurationMs), + autoModeHoldDurationMs(defAutoModeHoldDurationMs), + maxUniversalJamTimeMs(defMaxUniversalJamTimeMs), + fillJoystickHoldDurationMs(defFillJoystickHoldDurationMs), + fillPlungedWaitDurationMs(defFillPlungedWaitDurationMs), + fillHomedWaitDurationMs(defFillHomedWaitDurationMs), + recordHoldDurationMs(defRecordHoldDurationMs), + maxRecordDurationMs(defMaxRecordDurationMs), + replayDurationMs(defReplayDurationMs), + enablePostFlow(defEnablePostFlow), + postFlowDurationMs(defPostFlowDurationMs), + postFlowSpeedHz(defPostFlowSpeedHz), + currentPostFlowMa(defCurrentPostFlowMa), + postFlowStoppingWaitMs(defPostFlowStoppingWaitMs), + postFlowCompleteWaitMs(defPostFlowCompleteWaitMs), + defaultMaxOperationDurationMs(defDefaultMaxOperationDurationMs) + {} + + // Persistence methods + void toJson(JsonDocument& doc) const; + bool fromJson(const JsonObject& json); + bool load(const char* path = "/plunger.json"); + bool save(const char* path = "/plunger.json") const; + + // Debug method + void print() const; +}; + +#endif // PLUNGER_SETTINGS_H \ No newline at end of file diff --git a/src/components/PlungerStates.cpp b/src/components/PlungerStates.cpp new file mode 100644 index 00000000..08df7bea --- /dev/null +++ b/src/components/PlungerStates.cpp @@ -0,0 +1,559 @@ +#include "Plunger.h" +#include // For millis(), delay() + +void Plunger::_transitionToState(PlungerState newState) +{ + if (_currentState == newState) + return; // No change, do nothing further. + + Log.verboseln("[%s] State transition: %s -> %s", name.c_str(), _plungerStateToString(_currentState), _plungerStateToString(newState)); + PlungerState oldState = _currentState; + + // Specific exit logic for states + if (oldState == PlungerState::POST_FLOW) { + // Clean up if we are LEAVING post_flow, regardless of destination + _currentPostFlowState = PostFlowState::NONE; + _postFlowSubStateTimer.detach(); + } + + // Default timer resets, will be overridden by specific states if needed + // unsigned long newOperationStartTimeMs = 0; // Not needed here anymore + // unsigned long newCurrentMaxOperationTimeMs = 0; // Not needed here anymore + + if (newState != PlungerState::JAMMED) + { + _jammedStartTimeMs = 0; + } + + if (newState == PlungerState::IDLE || + newState == PlungerState::STOPPING || + newState == PlungerState::JAMMED || + newState == PlungerState::RESETTING_JAM || + newState == PlungerState::HOMING_AUTO || // Joystick hold reset for auto states too + newState == PlungerState::PLUNGING_AUTO) + { + _joystickHoldStartTimeMs = 0; + } + + // Specific entry logic for states + if (newState == PlungerState::POST_FLOW) { + Log.infoln("[%s] Initiating POST_FLOW sequence.", name.c_str()); + _vfdStop(); // Initial stop as per PostFlowState::POST_FLOW_STOPPING + _currentPostFlowState = PostFlowState::POST_FLOW_STOPPING; + _postFlowSubStateTimer.once_ms(_settings.postFlowStoppingWaitMs, &Plunger::_postFlowSubStateTimerRelay, this); + } + + if (newState == PlungerState::IDLE || newState == PlungerState::STOPPING) // General cleanup for IDLE/STOPPING + { + _currentFillState = FillState::NONE; + _fillSubStateTimer.detach(); + _currentPostFlowState = PostFlowState::NONE; // Also clean up post-flow sub-state if not already handled by exit logic + _postFlowSubStateTimer.detach(); + } + + // Specific state initializations for timers + if (newState == PlungerState::HOMING_MANUAL || newState == PlungerState::HOMING_AUTO || + newState == PlungerState::PLUNGING_MANUAL || newState == PlungerState::PLUNGING_AUTO) + { + // These states might have their own max operation times from the moment they are entered. + // Or, if a general max time for these states is desired from entry: + // newOperationStartTimeMs = millis(); + // newCurrentMaxOperationTimeMs = SOME_DEFAULT_MAX_FOR_THESE_STATES; // if applicable + } + + _currentState = newState; // Set the new state now + _lastStateChangeTimeMs = millis(); // Set last state change time now + + // Initialize/reset operation timers. They will be set specifically if the new state involves timed motor operation, + // or by the functions that start specific motor movements within a larger state (e.g., FILLING sub-states). + _operationStartTimeMs = 0; + _currentMaxOperationTimeMs = 0; + + // Specific entry logic for states regarding operation timers + switch (newState) + { + case PlungerState::HOMING_MANUAL: + case PlungerState::HOMING_AUTO: + case PlungerState::PLUNGING_MANUAL: + case PlungerState::PLUNGING_AUTO: + _operationStartTimeMs = millis(); + _currentMaxOperationTimeMs = _settings.defaultMaxOperationDurationMs; // Use from _settings + break; + + case PlungerState::FILLING: + _fillOperationStartTimeMs = millis(); // For the overall fill operation. + // Moving sub-states (PLUNGING, HOMING) will set their own _operationStartTimeMs + // and _currentMaxOperationTimeMs for the generic timeout check. + break; + + case PlungerState::POST_FLOW: + Log.infoln("[%s] Initiating POST_FLOW sequence.", name.c_str()); + _vfdStop(); + _currentPostFlowState = PostFlowState::POST_FLOW_STOPPING; + _postFlowSubStateTimer.once_ms(_settings.postFlowStoppingWaitMs, &Plunger::_postFlowSubStateTimerRelay, this); + // POST_FLOW_STARTING sub-state will set its own _operationStartTimeMs and _currentMaxOperationTimeMs. + break; + + default: + // For states like IDLE, STOPPING, JAMMED, RESETTING_JAM, timers remain 0. + break; + } + + if (newState == PlungerState::IDLE || newState == PlungerState::STOPPING) // General cleanup for IDLE/STOPPING + { + _currentFillState = FillState::NONE; + _fillSubStateTimer.detach(); + _currentPostFlowState = PostFlowState::NONE; // Also clean up post-flow sub-state if not already handled by exit logic + _postFlowSubStateTimer.detach(); + } + + // Specific state initializations for timers were here, now handled in switch or state handlers + + // Apply the determined timer values - ALREADY DONE IN SWITCH OR STATE HANDLERS + // _operationStartTimeMs = newOperationStartTimeMs; + // _currentMaxOperationTimeMs = newCurrentMaxOperationTimeMs; + + if (newState == PlungerState::HOMING_AUTO || newState == PlungerState::PLUNGING_AUTO) + { + _joystickReleasedSinceAutoStart = false; + } + + owner->onError(id, static_cast(newState)); +} + +void Plunger::_handleIdleState() +{ + Joystick::E_POSITION joyDir = static_cast(_joystick->getValue()); + if (joyDir == Joystick::E_POSITION::UP && _lastJoystickDirection != Joystick::E_POSITION::UP) + { + _joystickHoldStartTimeMs = millis(); + _vfdStartReverse(static_cast(_settings.speedSlowHz * 100.0f)); + _transitionToState(PlungerState::HOMING_MANUAL); + } + else if (joyDir == Joystick::E_POSITION::DOWN && _lastJoystickDirection != Joystick::E_POSITION::DOWN) + { + _joystickHoldStartTimeMs = millis(); + _vfdStartForward(static_cast(_calculatedPlungingSpeedHz)); + _transitionToState(PlungerState::PLUNGING_MANUAL); + } + else if (joyDir == Joystick::E_POSITION::LEFT && _lastJoystickDirection != Joystick::E_POSITION::LEFT) + { + if (_currentState == PlungerState::IDLE) { + _joystickHoldStartTimeMs = millis(); + _joystickFillHoldTimer.once_ms(_settings.fillJoystickHoldDurationMs, &Plunger::_joystickFillHoldTimerRelay, this); + } + } + else if (_lastJoystickDirection == Joystick::E_POSITION::LEFT && joyDir != Joystick::E_POSITION::LEFT) + { + if (_joystickHoldStartTimeMs != 0) { + _joystickFillHoldTimer.detach(); + _joystickHoldStartTimeMs = 0; + } + } + else if (joyDir == Joystick::E_POSITION::RIGHT && _lastJoystickDirection != Joystick::E_POSITION::RIGHT) + { + _joystickHoldStartTimeMs = millis(); + _joystickRecordHoldTimer.once_ms(_settings.recordHoldDurationMs, &Plunger::_joystickRecordHoldTimerRelay, this); + } + else if (_lastJoystickDirection == Joystick::E_POSITION::RIGHT && joyDir != Joystick::E_POSITION::RIGHT) + { + _joystickRecordHoldTimer.detach(); + unsigned long heldDuration = (_joystickHoldStartTimeMs > 0) ? (millis() - _joystickHoldStartTimeMs) : 0; + _joystickHoldStartTimeMs = 0; + if (heldDuration < _settings.recordHoldDurationMs && heldDuration > 50) { + if (_recordedPlungeDurationMs > 0) { + _transitionToState(PlungerState::REPLAY); + } + } + } + + if (_vfd->isRunning() && _joystickHoldStartTimeMs == 0 && joyDir == Joystick::E_POSITION::CENTER && _currentState == PlungerState::IDLE) + { + Log.warningln("[%s] IDLE: VFD unexpectedly running. Stopping.", name.c_str()); + _vfdStop(); + } +} + +void Plunger::_handleHomingManualState() +{ + Joystick::E_POSITION joyDir = static_cast(_joystick->getValue()); + unsigned long currentTimeMs = millis(); + if (joyDir == Joystick::E_POSITION::UP) + { + if (_autoModeEnabled && _joystickHoldStartTimeMs > 0 && (currentTimeMs - _joystickHoldStartTimeMs > _settings.autoModeHoldDurationMs)) // Use from _settings + { + _transitionToState(PlungerState::HOMING_AUTO); + } + } + else + { + _transitionToState(PlungerState::STOPPING); + } +} + +void Plunger::_handleHomingAutoState() +{ + Joystick::E_POSITION joyDir = static_cast(_joystick->getValue()); + Joystick::E_POSITION initialDir = Joystick::E_POSITION::UP; // For homing + + if (!_joystickReleasedSinceAutoStart) + { // Phase 1: Joystick hasn't been centered yet since auto start + if (joyDir == initialDir) + { + // Still holding initial direction, all good. + } + else if (joyDir == Joystick::E_POSITION::CENTER) + { + _joystickReleasedSinceAutoStart = true; // First release to center, mark and continue. + } + else + { + _transitionToState(PlungerState::STOPPING); + } + } + else + { // Phase 2: Joystick has been centered at least once + if (joyDir != Joystick::E_POSITION::CENTER) + { + // Joystick moved away from CENTER after being released there. This is the "change again". Abort. + _transitionToState(PlungerState::STOPPING); + } + // If joyDir is still CENTER, do nothing, continue auto mode. + } +} + +void Plunger::_handlePlungingManualState() +{ + Joystick::E_POSITION joyDir = static_cast(_joystick->getValue()); + unsigned long currentTimeMs = millis(); + + if (joyDir == Joystick::E_POSITION::DOWN) + { + if (_autoModeEnabled && _joystickHoldStartTimeMs > 0 && (currentTimeMs - _joystickHoldStartTimeMs > _settings.autoModeHoldDurationMs)) // Use from _settings + { + _transitionToState(PlungerState::PLUNGING_AUTO); + } + } + else + { + _transitionToState(PlungerState::STOPPING); + } +} + +void Plunger::_handlePlungingAutoState() +{ + Joystick::E_POSITION joyDir = static_cast(_joystick->getValue()); + Joystick::E_POSITION initialDir = Joystick::E_POSITION::DOWN; // For plunging + + if (!_joystickReleasedSinceAutoStart) + { // Phase 1: Joystick hasn't been centered yet since auto start + if (joyDir == initialDir) + { + // Still holding initial direction, all good. + } + else if (joyDir == Joystick::E_POSITION::CENTER) + { + _joystickReleasedSinceAutoStart = true; // First release to center, mark and continue. + } + else + { + // Moved from initial direction to something other than CENTER. Abort. + _transitionToState(PlungerState::STOPPING); + } + } + else + { // Phase 2: Joystick has been centered at least once + if (joyDir != Joystick::E_POSITION::CENTER) + { + _transitionToState(PlungerState::STOPPING); + } + // If joyDir is still CENTER, do nothing, continue auto mode. + } +} + +void Plunger::_handleStoppingState() +{ + _vfdStop(); + _joystickHoldStartTimeMs = 0; + _transitionToState(PlungerState::IDLE); +} + +void Plunger::_handleJammedState() +{ + _vfdResetJam(); + _vfdStop(); + _joystickHoldStartTimeMs = 0; + _transitionToState(PlungerState::RESETTING_JAM); + Joystick::E_POSITION joyDir = static_cast(_joystick->getValue()); +} + +void Plunger::_handleResettingJamState() +{ + Joystick::E_POSITION joyDir = static_cast(_joystick->getValue()); + if (joyDir == Joystick::E_POSITION::UP && _lastJoystickDirection != Joystick::E_POSITION::UP) + { + _vfdResetJam(); + _joystickHoldStartTimeMs = millis(); + _vfdStartReverse(static_cast(_settings.speedSlowHz * 100.0f)); + _transitionToState(PlungerState::HOMING_MANUAL); + } + else if (joyDir == Joystick::E_POSITION::CENTER) + { + // User is waiting or deciding, do nothing, VFD is stopped from _handleJammedState + } + else if (joyDir != Joystick::E_POSITION::UP && _lastJoystickDirection == Joystick::E_POSITION::CENTER) + { + // Joystick moved from CENTER to something other than UP (e.g., DOWN, LEFT, RIGHT) + // This is considered an intentional action to exit the JAMMED/RESETTING_JAM sequence without homing. + Log.infoln("[%s] Resetting Jam: Joystick moved from CENTER not to UP. Returning to IDLE. Manual VFD reset might be needed.", name.c_str()); + _vfdResetJam(); // Attempt reset one last time just in case + _transitionToState(PlungerState::IDLE); + } + // If joystick remains UP, or moves from UP to CENTER then back to UP, it stays in HOMING_MANUAL (or transitions there) + // If joystick is moved from UP to CENTER and stays CENTER, HOMING_MANUAL will transition to STOPPING then IDLE. +} + +void Plunger::_handleRecordState() +{ + Joystick::E_POSITION joyDir = static_cast(_joystick->getValue()); + if (_recordModeStartTimeMs == 0) { + // Log.infoln("[%s] RECORD: Starting plunge.", name.c_str()); + _vfdStartForward(static_cast(_calculatedPlungingSpeedHz)); + _recordModeStartTimeMs = millis(); // For calculating recorded duration + _operationStartTimeMs = millis(); // For generic timeout check + _currentMaxOperationTimeMs = _settings.maxRecordDurationMs; // Use from _settings + } + if (joyDir != Joystick::E_POSITION::RIGHT) { + _vfdStop(); + _recordedPlungeDurationMs = millis() - _recordModeStartTimeMs; + Log.infoln("[%s] RECORD complete. Duration: %lu ms.", name.c_str(), _recordedPlungeDurationMs); + _recordModeStartTimeMs = 0; + _operationStartTimeMs = 0; + _currentMaxOperationTimeMs = 0; + _transitionToState(PlungerState::IDLE); + return; + } +} + +void Plunger::_handleReplayState() +{ + if (_operationStartTimeMs == 0) { // First entry or re-entry after interruption for some reason + if (_recordedPlungeDurationMs <= 50) { // Check for a minimal valid duration + Log.warningln("[%s] REPLAY: Invalid or zero replay duration (%lu ms). -> IDLE", name.c_str(), _recordedPlungeDurationMs); + _transitionToState(PlungerState::IDLE); + return; + } + Log.infoln("[%s] REPLAY: Plunging for %lu ms.", name.c_str(), _recordedPlungeDurationMs); + _vfdStartForward(static_cast(_calculatedPlungingSpeedHz)); + _operationStartTimeMs = millis(); // For generic timeout check + _currentMaxOperationTimeMs = _recordedPlungeDurationMs; // Specific timeout for replay + _replayPlungeTimer.once_ms(_recordedPlungeDurationMs, &Plunger::_replayPlungeTimerRelay, this); + } + Joystick::E_POSITION joyDir = static_cast(_joystick->getValue()); + if (joyDir != Joystick::E_POSITION::CENTER && _operationStartTimeMs != 0) { + Log.infoln("[%s] REPLAY: Interrupted by joystick.", name.c_str()); + _vfdStop(); + _replayPlungeTimer.detach(); + _operationStartTimeMs = 0; + _transitionToState(PlungerState::STOPPING); + } +} + +void Plunger::_handleFillingState() +{ + Joystick::E_POSITION joyDir = static_cast(_joystick->getValue()); + // Log.verboseln("[%s] FILLING: JoyDir=%d, ReleasedSinceStart=%d, FillState=%s", + // name.c_str(), static_cast(joyDir), + // _joystickReleasedSinceAutoStart, + // _fillStateToString(_currentFillState)); // DEBUG REMOVED + + if (_currentState != PlungerState::FILLING) { + // Log.warningln("[%s] FILLING: Unexpected current state %s! -> IDLE", name.c_str(), _plungerStateToString(_currentState)); // DEBUG REMOVED (can be a regular warning if this case is problematic) + _currentFillState = FillState::NONE; + _fillSubStateTimer.detach(); + _transitionToState(PlungerState::IDLE); + return; + } + + if (!_joystickReleasedSinceAutoStart && joyDir == Joystick::E_POSITION::CENTER) { + // Log.infoln("[%s] FILLING: Joystick detected at CENTER. Setting ReleasedSinceAutoStart = true.", name.c_str()); // DEBUG REMOVED + _joystickReleasedSinceAutoStart = true; + } + + if (_joystickReleasedSinceAutoStart && joyDir != Joystick::E_POSITION::CENTER) { + // Log.infoln("[%s] FILLING sequence: Interrupted by joystick (JoyDir=%d after release). Current FillState: %s -> STOPPING", + // name.c_str(), static_cast(joyDir), _fillStateToString(_currentFillState)); // DEBUG REMOVED (original log was info, can be restored if needed) + _vfdStop(); + _fillSubStateTimer.detach(); + _currentFillState = FillState::NONE; + _transitionToState(PlungerState::STOPPING); + return; + } + // ... (placeholder for total fill operation timeout can remain commented) +} + +// Static relay implementations for Ticker callbacks +void Plunger::_joystickRecordHoldTimerRelay(Plunger* pThis) { + if (pThis) { + pThis->_onJoystickRecordHoldTimeout(); + } +} + +void Plunger::_replayPlungeTimerRelay(Plunger* pThis) { + if (pThis) { + pThis->_onReplayPlungeTimeout(); + } +} + +void Plunger::_fillSubStateTimerRelay(Plunger* pThis) { + if (pThis) { + pThis->_onFillSubStateTimeout(); + } +} + +// Actual timer event handler implementations +void Plunger::_onJoystickRecordHoldTimeout() { + if (static_cast(_joystick->getValue()) == Joystick::E_POSITION::RIGHT && _currentState == PlungerState::IDLE) { + Log.infoln("[%s] RECORD initiated from joystick.", name.c_str()); + _transitionToState(PlungerState::RECORD); + } + _joystickHoldStartTimeMs = 0; +} + +void Plunger::_onReplayPlungeTimeout() { + this->_vfdStop(); + this->_operationStartTimeMs = 0; + + if (_settings.enablePostFlow) { + Log.infoln("[%s] Replay plunge finished. Post-flow enabled. -> POST_FLOW", name.c_str()); + this->_transitionToState(PlungerState::POST_FLOW); + } else { + Log.infoln("[%s] Replay plunge finished. Post-flow disabled. -> IDLE", name.c_str()); + this->_transitionToState(PlungerState::IDLE); + } +} + +void Plunger::_onFillSubStateTimeout() { + if (_currentState != PlungerState::FILLING) { + Log.warningln("[%s] Fill Sub-State Timeout: Not in FILLING state. Aborting timer logic.", name.c_str()); + _fillSubStateTimer.detach(); + _currentFillState = FillState::NONE; + return; + } + switch (_currentFillState) { + case FillState::PLUNGED: + // Log.infoln("[%s] Fill Sub-State: PLUNGED wait over. -> HOMING.", name.c_str()); + _currentFillState = FillState::HOMING; + _vfdStartReverse(static_cast(_settings.speedFillHomeHz * 100.0f)); + _operationStartTimeMs = millis(); + _currentMaxOperationTimeMs = _settings.defaultMaxOperationDurationMs; + break; + case FillState::HOMED: + Log.infoln("[%s] FILLING sequence complete.", name.c_str()); + _currentFillState = FillState::NONE; + _transitionToState(PlungerState::IDLE); + break; + default: + Log.warningln("[%s] Fill Sub-State Timeout: Unhandled FillState: %s in PlungerState: %s. Aborting. -> IDLE", + name.c_str(), _fillStateToString(_currentFillState), _plungerStateToString(_currentState)); + _fillSubStateTimer.detach(); + _currentFillState = FillState::NONE; + _transitionToState(PlungerState::IDLE); + break; + } +} + +// Ticker relay and handler for Fill joystick hold +void Plunger::_joystickFillHoldTimerRelay(Plunger* pThis) { + if (pThis) { + pThis->_onJoystickFillHoldTimeout(); + } +} + +void Plunger::_onJoystickFillHoldTimeout() { + if (static_cast(_joystick->getValue()) == Joystick::E_POSITION::LEFT && _currentState == PlungerState::IDLE) { + Log.infoln("[%s] FILLING sequence initiated from joystick.", name.c_str()); + // _fillOperationStartTimeMs is set in _transitionToState(FILLING) + _currentFillState = FillState::PLUNGING; + _vfdStartForward(static_cast(_settings.speedFillPlungeHz * 100.0f)); + _operationStartTimeMs = millis(); + _currentMaxOperationTimeMs = _settings.defaultMaxOperationDurationMs; + _joystickReleasedSinceAutoStart = false; + _transitionToState(PlungerState::FILLING); + } + _joystickHoldStartTimeMs = 0; +} + +void Plunger::_handlePostFlowState() +{ + Joystick::E_POSITION joyDir = static_cast(_joystick->getValue()); + + // Check for joystick interruption - applies to all post-flow sub-states + if (_joystickReleasedSinceAutoStart && joyDir != Joystick::E_POSITION::CENTER) { + Log.infoln("[%s] POST_FLOW sequence: Interrupted by joystick. -> STOPPING", name.c_str()); + _currentPostFlowState = PostFlowState::NONE; // Ensure cleanup + _postFlowSubStateTimer.detach(); + _transitionToState(PlungerState::STOPPING); + return; + } + // Further joystick interaction logic might be needed depending on if held-down joy needs to abort etc. + + // Sub-state specific logic is primarily handled by the timer timeout (_onPostFlowSubStateTimeout) + // This loop handler can be used for continuous checks if any sub-state needs them (e.g., monitoring current outside of _checkVfdForJam) + // For now, most logic is event-driven by the timer. +} + +void Plunger::_onPostFlowSubStateTimeout() +{ + if (_currentState != PlungerState::POST_FLOW) { + Log.warningln("[%s] Post-Flow Sub-State Timeout: Not in POST_FLOW state (%s). Aborting timer logic.", name.c_str(), _plungerStateToString(_currentState)); + _postFlowSubStateTimer.detach(); + _currentPostFlowState = PostFlowState::NONE; + return; + } + + Log.verboseln("[%s] Post-Flow sub-state timeout. Current sub-state: %d", name.c_str(), static_cast(_currentPostFlowState)); + + switch (_currentPostFlowState) + { + case PostFlowState::POST_FLOW_STOPPING: + Log.infoln("[%s] Post-Flow: STOPPING wait complete. -> STARTING post-flow press.", name.c_str()); + _currentPostFlowState = PostFlowState::POST_FLOW_STARTING; + _vfdStartForward(static_cast(_settings.postFlowSpeedHz * 100.0f)); + _postFlowStartTimeMs = millis(); + _operationStartTimeMs = _postFlowStartTimeMs; + _currentMaxOperationTimeMs = _settings.postFlowDurationMs; + _postFlowSubStateTimer.once_ms(_settings.postFlowDurationMs, &Plunger::_postFlowSubStateTimerRelay, this); + break; + + case PostFlowState::POST_FLOW_STARTING: + Log.infoln("[%s] Post-Flow: STARTING (pressing) complete. -> COMPLETE wait.", name.c_str()); + _vfdStop(); + _currentPostFlowState = PostFlowState::POST_FLOW_COMPLETE; + _postFlowSubStateTimer.once_ms(_settings.postFlowCompleteWaitMs, &Plunger::_postFlowSubStateTimerRelay, this); + break; + + case PostFlowState::POST_FLOW_COMPLETE: + Log.infoln("[%s] Post-Flow: COMPLETE wait finished. Post-flow sequence fully complete. -> IDLE", name.c_str()); + _currentPostFlowState = PostFlowState::NONE; + _transitionToState(PlungerState::IDLE); + break; + + case PostFlowState::NONE: + default: + Log.warningln("[%s] Post-Flow Sub-State Timeout: Unhandled/NONE PostFlowState: %d. -> IDLE", + name.c_str(), static_cast(_currentPostFlowState)); + _postFlowSubStateTimer.detach(); + _currentPostFlowState = PostFlowState::NONE; + _transitionToState(PlungerState::IDLE); + break; + } +} + +// Static relay for post-flow sub-state timer +void Plunger::_postFlowSubStateTimerRelay(Plunger* pThis) { + if (pThis) { + pThis->_onPostFlowSubStateTimeout(); + } +} + diff --git a/src/components/Relay.h b/src/components/Relay.h new file mode 100644 index 00000000..44d42c21 --- /dev/null +++ b/src/components/Relay.h @@ -0,0 +1,180 @@ +#ifndef RELAY_H +#define RELAY_H + +#include +#include +#include +#include +#include "config.h" +#include "modbus/Modbus.h" +#include "config-modbus.h" +#include "modbus/ModbusTCP.h" +class Bridge; +class Relay : public Component +{ +private: // Keep address private, provide via getModbusInfo + const short modbusAddress; // Store Modbus address internally + MB_Registers m_modbus_block; + // m_modbus_view needs to be mutable to be returned as ModbusBlockView* from a const method. + mutable ModbusBlockView m_modbus_view; + +public: + Relay( + Component *owner, + short _pin, + short _id, + short _modbusAddress) + : Component("Relay", _id, Component::COMPONENT_DEFAULT, owner), + pin(_pin), + modbusAddress(_modbusAddress), + value(false) + { + setNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS); + + // Initialize instance-specific Modbus block. + // The modbusAddress is the actual start address for the Relay's single coil. + // So, the offset passed to the macro is 0. + m_modbus_block = INIT_MODBUS_BLOCK_TCP( + this->modbusAddress, // Base address for this component's block + 0, // Offset for this specific register + E_FN_CODE::FN_WRITE_COIL, // Function code + MB_ACCESS_READ_WRITE, // Access type + "Relay", // Name + nullptr // Group (nullptr if not applicable) + ); + + // Initialize the view to point to this instance-specific block + m_modbus_view.data = &m_modbus_block; // Point to the single block + m_modbus_view.count = 1; + } + + short info(short flags = 0, short val = 0) override + { + Log.verboseln("Relay::info - ID: %d, Pin: %d, Modbus Addr: %d, Value: %d, NetCaps: %d", + id, pin, modbusAddress, value, nFlags); + return E_OK; + } + + short debug() override + { + return info(0, 0); + } + + short setup() override + { + Component::setup(); // Call base class setup (important if it does network registration) + pinMode(pin, OUTPUT); + digitalWrite(pin, value); // Ensure pin state matches initial value + Log.verboseln("Relay::setup - ID %d, Pin %d, Initial Value: %d, Modbus Addr: %d", id, pin, value, modbusAddress); + return E_OK; + } + + short setValue(bool newValue) + { + if (value != newValue) + { + value = newValue; + digitalWrite(pin, newValue ? HIGH : LOW); + notifyStateChange(); + } + return E_OK; + } + + short setValueCmd(short arg1, short arg2) + { + return setValue(arg1 > 0); + } + + bool getValue() const + { + return value; + } + + /** + * @brief Handles writes coming from the network (e.g., Modbus write coil/register). + * @param reg The Modbus register being written to. + * @param networkValue The value received from the network. + * @return E_OK if the address matches and the value is set, E_INVALID_PARAMETER otherwise. + */ + short mb_tcp_write(MB_Registers *reg, short networkValue) override + { + return mb_tcp_write(reg->startAddress, networkValue); + } + /** + * @brief Handles writes coming from the network (e.g., Modbus write coil/register). + * @param address The Modbus address being written to (should match component's address). + * @param networkValue The value received from the network. + * @return E_OK if the address matches and the value is set, E_INVALID_PARAMETER otherwise. + */ + short mb_tcp_write(short address, short networkValue) override + { + if (address == modbusAddress) // Use internal member + { + bool newValue = (networkValue > 0); + if (value != newValue) + { + value = newValue; + digitalWrite(pin, value); // Re-enabled GPIO write + } + return E_OK; + } + return E_INVALID_PARAMETER; + } + + /** + * @brief Handles reads requests from the network (e.g., Modbus read coil/register). + * @param address The Modbus address being read (should match component's address). + * @return The current state (1 for ON, 0 for OFF) if the address matches, 0 otherwise. + */ + short mb_tcp_read(short address) override + { + if (address == modbusAddress) // Use internal member + { + return value ? 1 : 0; + } + return 0; // Default for mismatched addresses + } + + short mb_tcp_read(MB_Registers *reg) override + { + // Log.traceln(F("Relay::mb_tcp_read (Reg Context) - TCP Addr: %d, Type: %d"), reg->startAddress, reg->type); + return value ? 1 : 0; + } + + void mb_tcp_register(ModbusTCP *manager) const override + { + ModbusBlockView *blocksView = mb_tcp_blocks(); + Component *thiz = const_cast(this); + for (int i = 0; i < blocksView->count; ++i) + { + MB_Registers info = blocksView->data[i]; + manager->registerModbus(thiz, info); + } + } + + ModbusBlockView *mb_tcp_blocks() const override + { + // Return the instance-specific Modbus block view + return &m_modbus_view; + } + + short serial_register(Bridge *bridge) override + { + Component::serial_register(bridge); + bridge->registerMemberFunction(id, this, C_STR("setValue"), (ComponentFnPtr)&Relay::setValueCmd); + bridge->registerMemberFunction(id, this, C_STR("info"), (ComponentFnPtr)&Relay::info); + return E_OK; + } + + short loop() override + { + Component::loop(); + return E_OK; + } + + // --- Member Variables --- + const short pin; + bool value; +}; + +#endif diff --git a/src/components/SAKO_VFD.cpp b/src/components/SAKO_VFD.cpp new file mode 100644 index 00000000..e5c8e7bc --- /dev/null +++ b/src/components/SAKO_VFD.cpp @@ -0,0 +1,723 @@ +#include "config.h" + +#ifdef ENABLE_RS485 + +#include +#include "error_codes.h" +#include "components/SAKO_VFD.h" + +#include "modbus/ModbusTypes.h" +#include "modbus/Modbus.h" +#include "RS485.h" +#include "SakoTypes.h" +#include "Sako-Registers.h" +#include + +#define SAKO_MB_MONITOR_REGS 10 +#define SAKO_MB_TCP_OFFSET COMPONENT_KEY_SAKO_VFD * 10 + +// Placeholder for Frequency Setting Register - Replace with actual Sako Parameter Register Address +#define SAKO_REG_SET_FREQ 0x1000 + +// --- Define the Monitoring registers to read --- +// Read Running Freq, Set Freq, Bus Voltage, Output Voltage, Output Current, Fault Info, Running State, Fault Code +#define SAKO_VFD_READ_BLOCK_START_ADDR static_cast(E_SAKO_MON::E_SAKO_MON_RUNNING_FREQUENCY_HZ) // Start at U0-00 (1000) +// Calculate count based on the required registers. Need U0-00 to U0-04, plus U0-45, U0-61, U0-62 +// This is complex as they are not contiguous. Will require multiple read blocks or careful handling. +// For simplicity, let's *assume* a single block reading the first few essential ones for now. +// READ: Running Freq (1000), Set Freq (1001), Bus Volt (1002), Out Volt (1003), Out Curr (1004) +#define SAKO_VFD_READ_BLOCK_REG_COUNT 5 + + +// Enum for TCP Offsets is now in SAKO_VFD.h + +SAKO_VFD::SAKO_VFD(uint8_t slaveId, millis_t readInterval) + : RTU_Base(slaveId, COMPONENT_KEY_SAKO_VFD), + _readInterval(readInterval), + _vfdState(E_VFD_STATE_STOPPED) // Initialize VFD state +{ + componentId = id; + syncInterval = readInterval; + name = "SAKO_VFD[" + String(slaveId) + "]"; + setNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS); + const uint16_t tcpBaseAddr = mb_tcp_base_address(); + _modbusBlocks[0] = INIT_MODBUS_BLOCK(E_SakoTcpOffset::RUNNING_FREQUENCY, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, "SAKO: Run Freq", name.c_str()); + _modbusBlocks[1] = INIT_MODBUS_BLOCK(E_SakoTcpOffset::SET_FREQUENCY, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, "SAKO: Set Freq", name.c_str()); + _modbusBlocks[2] = INIT_MODBUS_BLOCK(E_SakoTcpOffset::OUTPUT_CURRENT, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, "SAKO: Current", name.c_str()); + _modbusBlocks[3] = INIT_MODBUS_BLOCK(E_SakoTcpOffset::OUTPUT_POWER_KW, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, "SAKO: Power kW", name.c_str()); + _modbusBlocks[4] = INIT_MODBUS_BLOCK(E_SakoTcpOffset::OUTPUT_TORQUE_PERCENT, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, "SAKO: Torque %", name.c_str()); + _modbusBlocks[5] = INIT_MODBUS_BLOCK(E_SakoTcpOffset::FAULT_CODE, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, "SAKO: Fault", name.c_str()); + _modbusBlocks[6] = INIT_MODBUS_BLOCK(E_SakoTcpOffset::IS_RUNNING, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, "SAKO: Running", name.c_str()); + _modbusBlocks[7] = INIT_MODBUS_BLOCK(E_SakoTcpOffset::HAS_FAULT, E_FN_CODE::FN_READ_HOLD_REGISTER, MB_ACCESS_READ_ONLY, "SAKO: Fault?", name.c_str()); + _modbusBlocks[8] = INIT_MODBUS_BLOCK(E_SakoTcpOffset::CMD_FREQ, E_FN_CODE::FN_WRITE_HOLD_REGISTER, MB_ACCESS_READ_WRITE, "SAKO: Set Freq Cmd", name.c_str()); + _modbusBlocks[9] = INIT_MODBUS_BLOCK(E_SakoTcpOffset::CMD_DIRECTION, E_FN_CODE::FN_WRITE_HOLD_REGISTER, MB_ACCESS_WRITE_ONLY, "SAKO: Direction Cmd", name.c_str()); + _modbusBlocks[10] = INIT_MODBUS_BLOCK(E_SakoTcpOffset::CMD_COMMAND, E_FN_CODE::FN_WRITE_HOLD_REGISTER, MB_ACCESS_READ_WRITE, "SAKO: Command", name.c_str()); + _modbusBlocks[11] = INIT_MODBUS_BLOCK(E_SakoTcpOffset::TARGET_REGISTER, E_FN_CODE::FN_WRITE_HOLD_REGISTER, MB_ACCESS_READ_WRITE, "SAKO: Target Reg", name.c_str()); + _modbusBlocks[12] = INIT_MODBUS_BLOCK(E_SakoTcpOffset::TARGET_VALUE, E_FN_CODE::FN_WRITE_HOLD_REGISTER, MB_ACCESS_READ_WRITE, "SAKO: Target Val", name.c_str()); + Log.infoln("SAKO_VFD: Modbus Blocks: Start Address %d", tcpBaseAddr); + static_assert(sizeof(_modbusBlocks) / sizeof(_modbusBlocks[0]) == SAKO_VFD::SAKO_TCP_BLOCK_COUNT, "Mismatch in _modbusBlocks size and SAKO_TCP_BLOCK_COUNT"); + _modbusBlockView = {_modbusBlocks, SAKO_TCP_BLOCK_COUNT}; +} + +short SAKO_VFD::setup() +{ + Log.infoln(F("SAKO_VFD[%d]: Setting up..."), slaveId); + + // --- Configure Mandatory Read Block --- + // Reads 10 registers starting from Running Frequency (U0-00) + // Covers: U0-00 to U0-09 (Running Freq to AI1 Voltage) + const uint16_t startAddr = static_cast(E_SAKO_MON::E_SAKO_MON_RUNNING_FREQUENCY_HZ); + const uint16_t regCount = 10; + ModbusReadBlock *block = addMandatoryReadBlock( + startAddr, + SAKO_MB_MONITOR_REGS, + E_FN_CODE::FN_READ_HOLD_REGISTER, // Assuming these are holding registers + _readInterval); + + if (!block) + { + Log.errorln(F("SAKO_VFD[%d]: Failed to add mandatory read block (Addr=0x%04X, Count=%d)!"), slaveId, startAddr, SAKO_MB_MONITOR_REGS); + return E_INVALID_PARAMETERS; + } + this->addOutputRegister(SAKO_REG_SET_FREQ, E_FN_CODE::FN_WRITE_HOLD_REGISTER, 0, PRIORITY_HIGH); + this->addOutputRegister(static_cast(E_SAKO_REGISTERS::E_SAKO_REGISTERS_SET_DIR), + E_FN_CODE::FN_WRITE_HOLD_REGISTER, + static_cast(E_SAKO_DIRECTION::E_SAKO_DIR_DECELERATION_STOP), + PRIORITY_HIGH); + + return E_OK; +} + +short SAKO_VFD::loop() +{ + // Placeholder for VFD specific logic in the main loop, if needed. + // For example, check for fault conditions based on _statusRegister or _faultCode + // and potentially trigger actions. + + // Check staleness example + // if (lastResponseTime > 0 && (millis() - lastResponseTime > (_readInterval * 2))) { + // Log.warningln(F("SAKO_VFD[%d]: Data might be stale (last response %lu ms ago)."), slaveId, millis() - lastResponseTime); + // } + + return 0; // Return non-zero to indicate an error +} + +short SAKO_VFD::info() +{ + uint16_t freq_int; // Raw frequency in 0.01 Hz + uint16_t speed, fault; + bool freqOk = getFrequency(freq_int); + bool speedOk = getSpeed(speed); + bool running = isRunning(); + bool faultState = hasFault(); + fault = getFaultCode(); + + Log.infoln(F("--- SAKO_VFD[%d] Info ---"), slaveId); + Log.infoln(F(" State: %s"), getStateString()); + Log.infoln(F(" Last Response: %lu ms ago"), (lastResponseTime > 0) ? (millis() - lastResponseTime) : 0); + Log.infoln(F(" Error Count: %u"), errorCount); + + char freqStr[10]; + // Convert 0.01 Hz uint16 to float Hz for display + float freq_display = freqOk ? (static_cast(freq_int) / 100.0f) : 0.0f; + dtostrf(freq_display, 5, 2, freqStr); // Format frequency e.g., 50.00 + Log.infoln(F(" Frequency: %s (%s Hz)"), freqOk ? "OK" : "Error/Missing", freqStr); + uint16_t current = 0; + bool currentOk = getOutputCurrent(current); + Log.infoln(F(" Raw Output Value: %s (%d)"), currentOk ? "OK" : "Error/Missing", current); + Log.infoln(F(" Speed: %s (%d RPM)"), speedOk ? "OK" : "Error/Missing", speedOk ? speed : 0); + Log.infoln(F(" Status: Running=%s, Fault=%s"), running ? "Yes" : "No", faultState ? "Yes" : "No"); + if (faultState) + { + Log.infoln(F(" Fault Code: 0x%04X"), fault); + } + Log.infoln(F(" Read Interval: %lu ms"), _readInterval); + Log.infoln(F("--- End SAKO_VFD[%d] Info --- "), slaveId); + return 0; +} + +void SAKO_VFD::onRegisterUpdate(uint16_t address, uint16_t newValue) +{ + // Update local state based on the register address using E_SAKO_MON enum + E_SAKO_MON reg = static_cast(address); + + switch (reg) + { + case E_SAKO_MON::E_SAKO_MON_RUNNING_FREQUENCY_HZ: // U0-00 + _currentFrequency = newValue; + _frequencyValid = true; + break; + case E_SAKO_MON::E_SAKO_MON_SET_FREQUENCY_HZ: // U0-01 + _setFrequency = newValue; + _setFrequencyValid = true; // Add a validity flag if needed + break; + case E_SAKO_MON::E_SAKO_MON_OUTPUT_CURRENT_A: // U0-04 + _currentCurrent = newValue; + _currentValid = true; + if (!isnan(_currentCurrent)) + { + _ampStats.add(_currentCurrent); + } + break; + case E_SAKO_MON::E_SAKO_MON_OUTPUT_POWER_KW: // U0-05 + _outputPowerKW = newValue; + _outputPowerKWValid = true; + break; + case E_SAKO_MON::E_SAKO_MON_OUTPUT_TORQUE_PERCENT: + _outputTorquePercent = newValue; + _outputTorquePercentValid = true; + break; + case E_SAKO_MON::E_SAKO_MON_AC_DRIVE_RUNNING_STATE: // U0-61 + _statusRegister = newValue; // Store the raw running state + _statusValid = true; + _updateStatusFromRegister(newValue); // Decode status bits + _updateVfdState(); // Update operational state based on status change + break; + case E_SAKO_MON::E_SAKO_MON_CURRENT_FAULT_CODE: // U0-62 + _faultCode = newValue; + _faultValid = true; + // Update fault status based on the code (0 means no fault) + _updateStatusFromRegister(_statusRegister); // Re-evaluate combined status if needed + _updateVfdState(); // Update operational state based on fault change + break; + default: + // Check if it's within the initial read block range for logging + if (address >= static_cast(E_SAKO_MON::E_SAKO_MON_RUNNING_FREQUENCY_HZ) && + address < (static_cast(E_SAKO_MON::E_SAKO_MON_RUNNING_FREQUENCY_HZ) + 5)) + { + // Log.traceln(F("SAKO_VFD[%d]: Internal update for known block register (Addr: 0x%04X) -> %d"), slaveId, address, newValue); + } + else + { + // Log.traceln(F("SAKO_VFD[%d]: Internal update for unhandled register (Addr: 0x%04X) -> %d"), slaveId, address, newValue); + } + break; + } + + // Update operational state if relevant data changed + if (reg == E_SAKO_MON::E_SAKO_MON_RUNNING_FREQUENCY_HZ || + reg == E_SAKO_MON::E_SAKO_MON_SET_FREQUENCY_HZ || + reg == E_SAKO_MON::E_SAKO_MON_AC_DRIVE_RUNNING_STATE || + reg == E_SAKO_MON::E_SAKO_MON_CURRENT_FAULT_CODE) + { + _updateVfdState(); + } + + // Call base class method + RTU_Base::onRegisterUpdate(address, newValue); +} + +bool SAKO_VFD::getFrequency(uint16_t &value) const +{ + if (_frequencyValid) + { + // Convert from 0.01 Hz to Hz with inverse scaling (divide by 0.64) + value = static_cast((_currentFrequency * 100) / 64); // Multiply by 100/64 ≈ 1.5625 + return true; + } + value = 0; + return false; +} + +bool SAKO_VFD::getSpeed(uint16_t &value) const +{ + if (_statusValid) + { + // Decode U0-61 (AC drive running state) based on Sako manual + // Example: Assuming 1 = Running FWD, 2 = Running REV, 0 = Stopped + if (_statusRegister == 1 || _statusRegister == 2) + { + value = 0; // Speed is not directly available in the running state + return true; + } + } + value = 0; + return false; +} + +bool SAKO_VFD::isRunning() const +{ + if (_statusValid) + { + // Check for any running state including forward, reverse, and jogging + return (_statusRegister == static_cast(E_SAKO_DIRECTION::E_SAKO_DIR_FWD) || + _statusRegister == static_cast(E_SAKO_DIRECTION::E_SAKO_DIR_REV) || + _statusRegister == static_cast(E_SAKO_DIRECTION::E_SAKO_DIR_JOGGING) || + _statusRegister == static_cast(E_SAKO_DIRECTION::E_SAKO_DIR_REVERSE_JOGGING)); + } + return false; +} + +bool SAKO_VFD::hasFault() const +{ + // Check fault code register U0-62 + // E_SAKO_ERROR_NO_FAULT is 0 + return _faultValid && (_faultCode != static_cast(E_SAKO_ERROR::E_SAKO_ERROR_NO_FAULT)); +} + +uint16_t SAKO_VFD::getFaultCode() const +{ + if (_faultValid) + { + return _faultCode; + } + return static_cast(E_SAKO_ERROR::E_SAKO_ERROR_NO_FAULT); // Return NO_FAULT if not valid +} + +E_VFD_STATE SAKO_VFD::getVfdState() const +{ + return _vfdState; +} + +bool SAKO_VFD::setFrequency(uint16_t value) +{ + uint16_t vfdValue = static_cast(value * 64); // 100 * 0.64 = 64 + Log.infoln(F("SAKO_VFD[%d]: Setting Frequency Output (Addr=%d, Val=%d Hz"), slaveId, SAKO_REG_SET_FREQ, value); + this->setOutputRegisterValue(SAKO_REG_SET_FREQ, vfdValue * 2); + return true; +} + +bool SAKO_VFD::run() // Assuming run means forward +{ + uint16_t cmd = static_cast(E_SAKO_DIRECTION::E_SAKO_DIR_FWD); + uint16_t reg = static_cast(E_SAKO_REGISTERS::E_SAKO_REGISTERS_SET_DIR); + // Log.infoln(F("SAKO_VFD[%d]: Setting RUN command (Addr=0x%04X, Val=0x%04X)"), slaveId, reg, cmd); + Log.infoln(F("SAKO_VFD[%d]: Setting RUN command (Addr=%d, Val=%d)"), slaveId, reg, cmd); + this->setOutputRegisterValue(reg, cmd); + return true; +} + +bool SAKO_VFD::reverse() +{ + uint16_t cmd = static_cast(E_SAKO_DIRECTION::E_SAKO_DIR_REV); + uint16_t reg = static_cast(E_SAKO_REGISTERS::E_SAKO_REGISTERS_SET_DIR); + // Log.infoln(F("SAKO_VFD[%d]: Setting REVERSE command (Addr=0x%04X, Val=0x%04X)"), slaveId, reg, cmd); + Log.infoln(F("SAKO_VFD[%d]: Setting Reverse command (Addr=%d, Val=%d)"), slaveId, reg, cmd); + this->setOutputRegisterValue(reg, cmd); + return true; +} + +bool SAKO_VFD::stop() // Using Deceleration Stop +{ + uint16_t cmd = static_cast(E_SAKO_DIRECTION::E_SAKO_DIR_DECELERATION_STOP); + uint16_t reg = static_cast(E_SAKO_REGISTERS::E_SAKO_REGISTERS_SET_DIR); + this->setOutputRegisterValue(reg, cmd); + return true; +} + +bool SAKO_VFD::resetFault() +{ + uint16_t cmd = static_cast(E_SAKO_DIRECTION::E_SAKO_DIR_FAULT_RESET); + uint16_t reg = static_cast(E_SAKO_REGISTERS::E_SAKO_REGISTERS_SET_DIR); + Log.infoln(F("SAKO_VFD[%d]: Setting RESET command (Addr=0x%04X, Val=0x%04X)"), slaveId, reg, cmd); + this->setOutputRegisterValue(reg, cmd); + return true; +} + +bool SAKO_VFD::retract() +{ + if (_retractState != E_VFD_RETRACT_STATE_NONE) + { + Log.warningln(F("SAKO_VFD[%d]: Already in retract sequence (State: %d)"), slaveId, _retractState); + return false; // Already retracting + } + + Log.infoln(F("SAKO_VFD[%d]: Starting retract sequence..."), slaveId); + // Initial step: Stop the motor (using deceleration stop) + if (stop()) + { + _retractState = E_VFD_RETRACT_STATE_BRAKING; // Transition to braking state + // Further state transitions will be handled in loop() + return true; + } + else + { + Log.errorln(F("SAKO_VFD[%d]: Failed to send initial stop command for retract."), slaveId); + return false; + } +} + +uint16_t SAKO_VFD::mb_tcp_base_address() const +{ + return SAKO_MB_TCP_OFFSET + (this->slaveId * SAKO_VFD_TCP_REG_RANGE); +} + +uint16_t SAKO_VFD::mb_tcp_offset_for_rtu_address(uint16_t rtuAddress) const +{ + E_SAKO_MON reg = static_cast(rtuAddress); + switch (reg) + { + case E_SAKO_MON::E_SAKO_MON_RUNNING_FREQUENCY_HZ: + return static_cast(E_SakoTcpOffset::RUNNING_FREQUENCY); + case E_SAKO_MON::E_SAKO_MON_SET_FREQUENCY_HZ: + return static_cast(E_SakoTcpOffset::SET_FREQUENCY); + case E_SAKO_MON::E_SAKO_MON_OUTPUT_CURRENT_A: + // Find the corresponding TCP offset if needed, e.g., OUTPUT_CURRENT + return static_cast(E_SakoTcpOffset::OUTPUT_CURRENT); // Example mapping + case E_SAKO_MON::E_SAKO_MON_AC_DRIVE_RUNNING_STATE: + // Update the IS_RUNNING boolean flag via TCP + return static_cast(E_SakoTcpOffset::IS_RUNNING); + case E_SAKO_MON::E_SAKO_MON_CURRENT_FAULT_CODE: + // Update both the FAULT_CODE and the HAS_FAULT flag + // How to signal multiple updates? Requires specific framework support or client interpretation. + // Broadcast the fault code first. Client might infer HAS_FAULT. + return static_cast(E_SakoTcpOffset::FAULT_CODE); + // Or consider broadcasting HAS_FAULT if it's a separate TCP register? + default: + return 0; // No direct broadcast mapping for other registers + } +} + +ModbusBlockView *SAKO_VFD::mb_tcp_blocks() const +{ + return const_cast(&_modbusBlockView); +} + +short SAKO_VFD::mb_tcp_read(MB_Registers *reg) +{ + if (!reg) + { + Log.errorln(F("SAKO_VFD[%d]::mb_tcp_read - Invalid MB_Registers pointer"), this->slaveId); + return (short)MB_Error::ServerDeviceFailure; + } + + const uint16_t instanceBaseAddr = this->mb_tcp_base_address(); + if (instanceBaseAddr == 0) + { // Handle cases where TCP mapping might not be configured + Log.errorln(F("SAKO_VFD[%d]::mb_tcp_read - TCP Base Address is 0"), this->slaveId); + return (short)MB_Error::ServerDeviceFailure; + } + + const short requestedAddress = reg->startAddress; + short offset = requestedAddress - instanceBaseAddr; + if (offset < 1 || offset > SAKO_VFD_TCP_REG_RANGE) // Use defined range + { + Log.warningln(F("SAKO_VFD[%d]: Read invalid offset %d (Addr %d, Base %d)"), + this->slaveId, offset, requestedAddress, instanceBaseAddr); + return (short)MB_Error::IllegalDataAddress; + } + + uint16_t value = 0; + bool success = false; + E_SakoTcpOffset regOffset = static_cast(offset); + + switch (regOffset) + { + case E_SakoTcpOffset::RUNNING_FREQUENCY: + success = getFrequency(value); + if (!success) + value = 0xFFFF; + break; + case E_SakoTcpOffset::SET_FREQUENCY: + success = _setFrequencyValid; + value = success ? (_setFrequency / 100) : 0xFFFF; + break; + case E_SakoTcpOffset::OUTPUT_CURRENT: + { + success = getOutputCurrent(value); + if (!success) + value = 0xFFFF; + } + break; + case E_SakoTcpOffset::OUTPUT_POWER_KW: + success = getOutputPowerKW(value); + if (!success) + value = 0xFFFF; + break; + case E_SakoTcpOffset::OUTPUT_TORQUE_PERCENT: + success = getOutputTorquePercent(value); + if (!success) + value = 0xFFFF; + break; + case E_SakoTcpOffset::FAULT_CODE: + value = getFaultCode(); // Use getter which returns 0 for no fault/invalid + success = true; // Reading fault code status is always possible via getter + break; + case E_SakoTcpOffset::IS_RUNNING: + value = isRunning() ? 1 : 0; + success = _statusValid; // Depends on status being valid + break; + case E_SakoTcpOffset::HAS_FAULT: + value = hasFault() ? 1 : 0; + success = _faultValid; // Depends on fault code being valid + break; + case E_SakoTcpOffset::CMD_FREQ: // Read associated with write freq command (returns current *set* freq) + // Return the raw set frequency value (0.01 Hz units) + success = _setFrequencyValid; // Corrected: was _setFrequencyValid / 100 + value = success ? (_setFrequency / 100) : 0xFFFF; // Display in Hz + break; + case E_SakoTcpOffset::CMD_DIRECTION: // Read associated with command write returns current running state? + value = _statusValid ? _statusRegister : 0xFFFF; // Return raw status + success = _statusValid; + break; + case E_SakoTcpOffset::TARGET_REGISTER: + value = _tcpTargetRegister; // Return the stored target register + success = true; + break; + case E_SakoTcpOffset::TARGET_VALUE: + value = 0; // This register is write-only for triggering, reads as 0 + success = true; + break; + default: + return E_OK; + // Log.warningln(F("SAKO_VFD[%d]: TCP Read unhandled offset %d"), this->slaveId, offset); + // return (short)MB_Error::IllegalDataAddress; + } + + return success ? value : 0xFFFF; // Return value or error indicator +} + +short SAKO_VFD::setupVFD() +{ + RS485 *rs485 = (RS485 *)owner; + rs485->modbus.writeRegister(MB_SAKO_VFD_SLAVE_ID, (uint16_t)E_SAKO_PARAM::E_SAKO_PARAM_P00_03_MAIN_FREQUENCY_SOURCE_X_SELECTION, 9, true); + rs485->modbus.writeRegister(MB_SAKO_VFD_SLAVE_ID, (uint16_t)E_SAKO_PARAM::E_SAKO_PARAM_P00_02_COMMAND_SOURCE_SELECTION, 2, true); + rs485->modbus.writeRegister(MB_SAKO_VFD_SLAVE_ID, (uint16_t)E_SAKO_PARAM::E_SAKO_PARAM_P00_10_MAXIMUM_FREQUENCY, 7500, true); + rs485->modbus.writeRegister(MB_SAKO_VFD_SLAVE_ID, (uint16_t)E_SAKO_PARAM::E_SAKO_PARAM_P00_17_ACCELERATION_TIME_1, 10, true); + rs485->modbus.writeRegister(MB_SAKO_VFD_SLAVE_ID, (uint16_t)E_SAKO_PARAM::E_SAKO_PARAM_P00_18_DECELERATION_TIME_1, 10, true); + rs485->modbus.writeRegister(MB_SAKO_VFD_SLAVE_ID, (uint16_t)E_SAKO_PARAM::E_SAKO_PARAM_P1_01_RATED_MOTOR_POWER, 2, true); + rs485->modbus.writeRegister(MB_SAKO_VFD_SLAVE_ID, (uint16_t)E_SAKO_PARAM::E_SAKO_PARAM_P1_02_RATED_MOTOR_VOLTAGE, 400, true); + // rs485->modbus.writeRegister(MB_SAKO_VFD_SLAVE_ID, (uint16_t)E_SAKO_PARAM::E_SAKO_PARAM_P1_04_RATED_MOTOR_FREQUENCY, 5000, true); + + return E_OK; +} + +short SAKO_VFD::serial_register(Bridge *bridge) +{ + Component::serial_register(bridge); + bridge->registerMemberFunction(id, this, C_STR("info"), (ComponentFnPtr)&SAKO_VFD::info); + bridge->registerMemberFunction(id, this, C_STR("setupVFD"), (ComponentFnPtr)&SAKO_VFD::setupVFD); + return E_OK; +} + +short SAKO_VFD::mb_tcp_write(MB_Registers *reg, short value) +{ + if (!reg) + return E_INVALID_PARAMETER; + + const uint16_t tcpBaseAddr = this->mb_tcp_base_address(); + if (tcpBaseAddr == 0) + { + Log.errorln(F("SAKO_VFD[%d]::mb_tcp_write - TCP Base Address is 0"), this->slaveId); + return (short)MB_Error::ServerDeviceFailure; + } + + const short requestedTcpAddress = reg->startAddress; + short offset = requestedTcpAddress - tcpBaseAddr; + E_SakoTcpOffset regOffset = static_cast(offset); + + bool commandSuccess = false; + + switch (regOffset) + { + case E_SakoTcpOffset::CMD_FREQ: + commandSuccess = setFrequency(static_cast(value)); + break; + case E_SakoTcpOffset::CMD_DIRECTION: + { + E_SAKO_DIRECTION directionCmd = static_cast(value); + uint16_t regAddr; + + switch (directionCmd) + { + case E_SAKO_DIRECTION::E_SAKO_DIR_FWD: + commandSuccess = run(); // Assumes run() sends FWD + break; + case E_SAKO_DIRECTION::E_SAKO_DIR_REV: + commandSuccess = reverse(); + break; + case E_SAKO_DIRECTION::E_SAKO_DIR_DECELERATION_STOP: + commandSuccess = stop(); // Assumes stop() sends DECEL_STOP + break; + case E_SAKO_DIRECTION::E_SAKO_DIR_FREE_STOP: + regAddr = static_cast(E_SAKO_REGISTERS::E_SAKO_REGISTERS_SET_DIR); + Log.infoln(F("SAKO_VFD[%d]: Setting FREE STOP command (Addr=0x%04X, Val=0x%04X)"), slaveId, regAddr, value); + this->setOutputRegisterValue(regAddr, value); // Send the raw command value + commandSuccess = true; + break; + case E_SAKO_DIRECTION::E_SAKO_DIR_FAULT_RESET: + commandSuccess = resetFault(); + break; + default: + Log.warningln(F("SAKO_VFD[%d]: Unknown TCP direction command value %d"), this->slaveId, value); + commandSuccess = false; // Invalid command value + break; + } + } + break; + case E_SakoTcpOffset::TARGET_REGISTER: + _tcpTargetRegister = static_cast(value); + Log.infoln(F("SAKO_VFD[%d]: TCP Target Register set to 0x%04X"), this->slaveId, _tcpTargetRegister); + commandSuccess = true; + break; + case E_SakoTcpOffset::TARGET_VALUE: + if (_tcpTargetRegister == 0) + { + Log.warningln(F("SAKO_VFD[%d]: TCP Target Value write ignored, Target Register not set (is 0)."), this->slaveId); + commandSuccess = false; + } + else + { + RS485 *rs485 = (RS485 *)owner; + if (rs485) + { + Log.infoln(F("SAKO_VFD[%d]: TCP Writing Value 0x%04X to VFD Register 0x%04X"), this->slaveId, value, _tcpTargetRegister); + MB_Error writeStatus = rs485->modbus.writeRegister(this->slaveId, _tcpTargetRegister, static_cast(value)); + commandSuccess = (writeStatus == MB_Error::Success); + if (commandSuccess) + { + Log.infoln(F("SAKO_VFD[%d]: VFD Register 0x%04X write successful."), this->slaveId, _tcpTargetRegister); + } + else + { + Log.errorln(F("SAKO_VFD[%d]: VFD Register 0x%04X write failed. Status: %d"), this->slaveId, _tcpTargetRegister, static_cast(writeStatus)); + } + // Reset _tcpTargetRegister after the write attempt, regardless of success or failure, as per user requirement for registers to be 0 after write. + _tcpTargetRegister = 0; + } + else + { + Log.errorln(F("SAKO_VFD[%d]: RS485 owner not found for VFD write."), this->slaveId); + commandSuccess = false; + _tcpTargetRegister = 0; // Also reset if owner not found, to ensure it's cleared. + } + } + break; + default: + Log.warningln(F("SAKO_VFD[%d]: Unknown TCP write offset %d"), this->slaveId, offset); + commandSuccess = false; + break; + } + + return commandSuccess ? (short)MB_Error::Success : (short)MB_Error::ServerDeviceFailure; +} + +// --- Internal Helper Methods --- + +void SAKO_VFD::_updateStatusFromRegister(uint16_t statusReg) +{ + // Decode U0-61 (AC drive running state) according to Sako manual + // This register value might directly represent running/stopped/fault states + // Example: (Check Manual!) + // 0: Stopped + // 1: Running Forward + // 2: Running Reverse + // 3: Faulted? (Maybe combined with U0-62) + + // Update internal state flags based on the decoded value + // This is just an example based on common VFD status registers + bool currentRunning = (statusReg == 1 || statusReg == 2); + // Fault status is primarily determined by _faultCode from U0-62 + + // Log changes if significant + // if (currentRunning != isRunning()) { + // Log.infoln(F("SAKO_VFD[%d]: Running state changed to %s"), slaveId, currentRunning ? "Running" : "Stopped"); + // } + + // Note: `_statusValid` is set when U0-61 is received in onRegisterUpdate. + // Note: `_faultValid` and `_faultCode` are set when U0-62 is received. +} + +// --- Internal Helper: Update VFD Operational State --- +void SAKO_VFD::_updateVfdState() +{ + const uint16_t FREQUENCY_TOLERANCE = 5; // Tolerance for frequency comparison (0.05 Hz) + E_VFD_STATE previousState = _vfdState; + + if (hasFault()) + { + _vfdState = E_VFD_STATE_ERROR; + } + else if (!isRunning()) + { + // If not running (based on U0-61) and current freq is near zero, consider it stopped + // Otherwise, it might be decelerating after a stop command + if (_frequencyValid && _currentFrequency <= FREQUENCY_TOLERANCE) + { + _vfdState = E_VFD_STATE_STOPPED; + } + else + { + // Still spinning down after stop command or just stopped + _vfdState = E_VFD_STATE_DECELERATING; + } + } + else + { // isRunning() is true + if (_frequencyValid && _setFrequencyValid) + { + // Compare current frequency to set frequency + if (_currentFrequency < (_setFrequency - FREQUENCY_TOLERANCE)) + { + _vfdState = E_VFD_STATE_ACCELERATING; + } + else if (_currentFrequency > (_setFrequency + FREQUENCY_TOLERANCE)) + { + // Could be decelerating due to setpoint change or overshoot + _vfdState = E_VFD_STATE_DECELERATING; + } + else + { + // Frequencies are close enough - considered running at setpoint + _vfdState = E_VFD_STATE_RUNNING; + } + } + else + { + // Running, but frequency data is missing/invalid - default to RUNNING state? + // Or introduce an UNKNOWN state? For now, assume RUNNING. + _vfdState = E_VFD_STATE_RUNNING; + } + } + + // Optional: Log state changes + if (_vfdState != previousState) + { + Log.infoln(F("SAKO_VFD[%d]: VFD State changed from %d to %d"), slaveId, previousState, _vfdState); + } +} + +short SAKO_VFD::getTorque() +{ + if (!isRunning()) + return E_INVALID_PARAMETER; + float torque = _currentCurrent * _currentFrequency; + return E_OK; +} + +short SAKO_VFD::reset() { return E_OK; } + +bool SAKO_VFD::getOutputCurrent(uint16_t &value) const +{ + if (_currentValid) + { + value = static_cast(_currentCurrent); // _currentCurrent now holds the raw value as a float + return true; + } + value = 0; // Default if not valid + return false; +} + +bool SAKO_VFD::getOutputPowerKW(uint16_t &value) const +{ + if (_outputPowerKWValid) + { + value = _outputPowerKW; + return true; + } + value = 0; + return false; +} + +bool SAKO_VFD::getOutputTorquePercent(uint16_t &value) const +{ + if (_outputTorquePercentValid) + { + value = _outputTorquePercent; + return true; + } + value = 0; + return false; +} + +#endif // ENABLE_RS485 \ No newline at end of file diff --git a/src/components/SAKO_VFD.h b/src/components/SAKO_VFD.h new file mode 100644 index 00000000..fa724d17 --- /dev/null +++ b/src/components/SAKO_VFD.h @@ -0,0 +1,147 @@ +#ifndef SAKO_VFD_H +#define SAKO_VFD_H + +#include "config.h" + +#ifdef ENABLE_RS485 // Assuming this VFD also uses RS485 + +#include +#include +#include "modbus/ModbusRTU.h" +#include "modbus/ModbusTypes.h" +#include + +#define SAKO_VFD_DEFAULT_READ_INTERVAL 100 +#define SAKO_VFD_TCP_REG_RANGE 16 + +// Enum for Retract State Machine +typedef enum +{ + E_VFD_RETRACT_STATE_NONE = 0, + E_VFD_RETRACT_STATE_BRAKING = 1, + E_VFD_RETRACT_STATE_STOPPED = 2, + E_VFD_RETRACT_STATE_REVERSING = 3, + E_VFD_RETRACT_STATE_BRAKE_REVERSING = 4, + E_VFD_RETRACT_STATE_RETRACTED = 5, +} E_VFD_RETRACT_STATE; + +// Enum for VFD Operational State +typedef enum +{ + E_VFD_STATE_STOPPED = 1, + E_VFD_STATE_DECELERATING = 2, + E_VFD_STATE_RUNNING = 3, + E_VFD_STATE_ACCELERATING = 4, + E_VFD_STATE_ERROR = 8 +} E_VFD_STATE; + +enum class E_SakoTcpOffset : ushort +{ + RUNNING_FREQUENCY = 1, // Corresponds to U0-00 + SET_FREQUENCY = 2, // Corresponds to U0-01 + OUTPUT_CURRENT = 3, // Corresponds to U0-04 + OUTPUT_POWER_KW = 4, // Corresponds to U0-05 + OUTPUT_TORQUE_PERCENT = 5, // Corresponds to U0-06 + FAULT_CODE = 6, // Corresponds to U0-62 + IS_RUNNING = 7, // Derived from U0-61 + HAS_FAULT = 8, // Derived from U0-62 + CMD_FREQ = 9, // Write Frequency (Uses SAKO_REG_SET_FREQ) + CMD_DIRECTION = 10, // Write Direction/Control (Uses E_SAKO_REGISTERS_SET_DIR) + CMD_COMMAND = 11, // Internal command + TARGET_REGISTER = 12, // Write target register address + TARGET_VALUE = 13, // Write value to target register +}; + +class SAKO_VFD : public RTU_Base +{ +public: + static constexpr int SAKO_TCP_BLOCK_COUNT = 13; + // Constructor + SAKO_VFD(uint8_t slaveId, millis_t readInterval = SAKO_VFD_DEFAULT_READ_INTERVAL); + virtual ~SAKO_VFD() = default; + // --- Component Interface --- + virtual short setup() override; + virtual short loop() override; + virtual short info() override; + short reset(); + short getTorque(); + + // --- Modbus Register Update Notification --- + virtual void onRegisterUpdate(uint16_t address, uint16_t newValue) override; + + // --- Getters for Specific VFD Values --- + // Add getters relevant to a VFD, e.g., Frequency, Speed, Status, Fault codes + bool getFrequency(uint16_t& value) const; // Returns frequency in 0.01 Hz units + bool getSpeed(uint16_t& value) const; // Example: speed might be integer RPM + bool isRunning() const; + bool hasFault() const; + uint16_t getFaultCode() const; + E_VFD_STATE getVfdState() const; + bool getOutputPowerKW(uint16_t& value) const; + bool getOutputTorquePercent(uint16_t& value) const; + bool getOutputCurrent(uint16_t& value) const; + + // --- Setters for VFD Control --- + bool setFrequency(uint16_t value); // Expects frequency in 0.01 Hz units + bool run(); + bool reverse(); + bool stop(); + bool resetFault(); + bool retract(); + + // --- Modbus Block Definitions --- + virtual ModbusBlockView* mb_tcp_blocks() const override; + virtual short mb_tcp_read(MB_Registers * reg) override; + virtual short mb_tcp_write(MB_Registers * reg, short value) override; + + // --- Modbus TCP Mapping Overrides --- + virtual uint16_t mb_tcp_base_address() const override; + virtual uint16_t mb_tcp_offset_for_rtu_address(uint16_t rtuAddress) const override; + + +private: + millis_t _readInterval; + + // --- Local State Storage --- + // Store relevant VFD parameters and their validity flags + uint16_t _currentFrequency = 0; // 0.01 Hz units + uint16_t _currentSpeed = 0; + uint16_t _setFrequency = 0; // 0.01 Hz units + uint16_t _currentCurrent = 0; + uint16_t _statusRegister = 0; // Example status register + uint16_t _faultCode = 0; + uint16_t _outputPowerKW = 0; + uint16_t _outputTorquePercent = 0; + bool _frequencyValid = false; + bool _speedValid = false; + bool _setFrequencyValid = false; + bool _currentValid = false; + bool _statusValid = false; + bool _faultValid = false; + bool _outputPowerKWValid = false; + bool _outputTorquePercentValid = false; + + uint16_t _tcpTargetRegister = 0; // Stores the value written to TARGET_REGISTER offset + + // Retract State + E_VFD_RETRACT_STATE _retractState = E_VFD_RETRACT_STATE_NONE; + + // Operational State + E_VFD_STATE _vfdState = E_VFD_STATE_STOPPED; + + // Statistics + Statistic _ampStats; + + MB_Registers _modbusBlocks[SAKO_TCP_BLOCK_COUNT]; + mutable ModbusBlockView _modbusBlockView; + bool _modbusBlocksInitialized; // Added for on-demand initialization + + // Add internal helper methods if needed + void _updateStatusFromRegister(uint16_t statusReg); + void _updateVfdState(); + short serial_register(Bridge *bridge) override; + short setupVFD(); +}; + +#endif // ENABLE_RS485 +#endif // SAKO_VFD_H \ No newline at end of file diff --git a/src/components/Sako-Registers.h b/src/components/Sako-Registers.h new file mode 100644 index 00000000..b7a31bce --- /dev/null +++ b/src/components/Sako-Registers.h @@ -0,0 +1,937 @@ +enum class E_SAKO_PARAM : int +{ + // Parameter P0-01 | Name : Motor Control Mode + // Values : + // 0 : Sensorless, + // 2 : V/F - Servo + E_SAKO_PARAM_P00_01_MOTOR_CONTROL_MODE = 0xF001, + + // Parameter P0-02 | Name : Command source selection + // Values : + // 0 : Operation panel control (LED off), + // 1 : Terminal control (LED on), + // 2 : Communication control (LED blinking) + E_SAKO_PARAM_P00_02_COMMAND_SOURCE_SELECTION = 0xF002, + + // Parameter P0-03 | Name : Main frequency source X selection + // Values : + // 0 : Digital setting (preset frequency P0-08, press UP/DOWN to modify, non-retentive at power failure), + // 1 : Digital setting (preset frequency P0-08, press UP/DOWN to modify, retentive at power failure), + // 2 : AI1, + // 3 : Panel potentiometer, + // 4 : External panel potentiometer, + // 5 : HDI pulse setting (DI5), + // 6 : Multi-command, + // 7 : Simple PLC, + // 8 : PID, + // 9 : Communication setting + E_SAKO_PARAM_P00_03_MAIN_FREQUENCY_SOURCE_X_SELECTION = 0xF003, + + // Parameter P0-04 | Name : Auxiliary frequency source Y selection + // Values : + // 0 : Digital setting (preset frequency P0-08, press UP/DOWN to modify, non-retentive at power failure), + // 1 : Digital setting (preset frequency P0-08, press UP/DOWN to modify, retentive at power failure), + // 2 : AI1, + // 3 : Panel potentiometer, + // 4 : External panel potentiometer, + // 5 : HDI pulse setting (DI5), + // 6 : Multi-command, + // 7 : Simple PLC, + // 8 : PID, + // 9 : Communication setting + E_SAKO_PARAM_P00_04_AUXILIARY_FREQUENCY_SOURCE_Y_SELECTION = 0xF004, + + // Parameter P0-05 | Name : Selection of Y range of auxiliary frequency source in superposition + // Values : + // 0 : Relative to maximum frequency, + // 1 : Relative to main frequency X + E_SAKO_PARAM_P00_05_SELECTION_OF_Y_RANGE_OF_AUXILIARY_FREQUENCY_SOURCE_IN_SUPERPOSITION = 0xF005, + + // Parameter P0-06 | Name : Selection of Y range of auxiliary frequency source in superposition + // Values : + // This parameter accepts a value from 0 to 150, + // representing a percentage from 0% to 150%. + // Note: The name appears to be a duplicate of P0-05 in the provided text, but assuming it's distinct as per P0-06. + // Given the description for P0-06 in the original text, the name might be "Auxiliary frequency Y gain in superposition" or similar. + // For consistency, I'm using the name as provided: "Selection of Y range of auxiliary frequency source in superposition" + E_SAKO_PARAM_P00_06_AUXILIARY_FREQUENCY_Y_GAIN_IN_SUPERPOSITION = 0xF006, // Adjusted name based on typical use for a percentage range + + // Parameter P0-07 | Name : Frequency source superposition selection + // Values : + // Unit's digit (Frequency source selection) + // 0 : Main frequency source X + // 1 : X and Y operation (operation relationship determined by ten's digit) + // 2 : Switchover between X and Y + // 3 : Switchover between X and "X and Y operation" + // 4 : Switchover between Y and "X and Y operation" + // Ten's digit (X and Y operation relationship) + // 0 : X+Y + // 1 : X-Y + // 2 : Maximum + // 3 : Minimum + E_SAKO_PARAM_P00_07_FREQUENCY_SOURCE_SUPERPOSITION_SELECTION = 0xF007, + + // Parameter P0-08 | Name : Preset frequency + // Values : + // This parameter accepts a value from 0.00Hz to maximum frequency (P0-10). + E_SAKO_PARAM_P00_08_PRESET_FREQUENCY = 0xF008, + + // Parameter P0-09 | Name : Rotation direction + // Values : + // 0 : Same direction + // 1 : Reverse direction + E_SAKO_PARAM_P00_09_ROTATION_DIRECTION = 0xF009, + + // Parameter P0-10 | Name : Maximum frequency + // Values : + // This parameter accepts a value from 5.00Hz to 500.00Hz. + E_SAKO_PARAM_P00_10_MAXIMUM_FREQUENCY = 0xF00A, + + // Parameter P0-11 | Name : Source of frequency upper limit + // Values : + // 0 : Set by P0-12 + // 1 : AI1 + // 2 : AI2 local potentiometer + // 3 : AI3 panel potentiometer external keyboard potentiometer + // 4 : HDI pulse setting + // 5 : Communication setting + E_SAKO_PARAM_P00_11_SOURCE_OF_FREQUENCY_UPPER_LIMIT = 0xF00B, + + // Parameter P0-12 | Name : Frequency upper limit + // Values : + // This parameter sets the frequency upper limit, ranging from Frequency lower limit (P0-14) to maximum frequency (P0-10). + E_SAKO_PARAM_P00_12_FREQUENCY_UPPER_LIMIT = 0xF00C, + + // Parameter P0-13 | Name : Frequency upper limit offset + // Values : + // This parameter accepts a value from 0.00Hz to maximum frequency (P0-10). + E_SAKO_PARAM_P00_13_FREQUENCY_UPPER_LIMIT_OFFSET = 0xF00D, + + // Parameter P0-14 | Name : Frequency lower limit + // Values : + // This parameter sets the frequency lower limit, ranging from 0.00Hz to frequency upper limit (P0-12). + E_SAKO_PARAM_P00_14_FREQUENCY_LOWER_LIMIT = 0xF00E, + + // Parameter P0-15 | Name : Carrier frequency + // Values : + // This parameter accepts a value from 2.0kHz to 8.0kHz. + E_SAKO_PARAM_P00_15_CARRIER_FREQUENCY = 0xF00F, + + // Parameter P0-16 | Name : Carrier frequency adjustment with temperature + // Values : + // 0 : No, + // 1 : Yes + E_SAKO_PARAM_P00_16_CARRIER_FREQUENCY_ADJUSTMENT_WITH_TEMPERATURE = 0xF010, + + // Parameter P0-17 | Name : Acceleration time 1 + // Values : + // 0.00s ~ 650.00s (when P0-19=2), + // 0.0s ~ 6500.0s (when P0-19=1), + // 0s ~ 65000s (when P0-19=0) + E_SAKO_PARAM_P00_17_ACCELERATION_TIME_1 = 0xF011, + + // Parameter P0-18 | Name : Deceleration time 1 + // Values : + // 0.00s ~ 650.00s (when P0-19=2), + // 0.0s ~ 6500.0s (when P0-19=1), + // 0s ~ 65000s (when P0-19=0) + E_SAKO_PARAM_P00_18_DECELERATION_TIME_1 = 0xF012, + + // Parameter P0-19 | Name : Acceleration/Deceleration time unit + // Values : + // 0 : 1s, + // 1 : 0.1s, + // 2 : 0.01s + E_SAKO_PARAM_P00_19_ACCELERATION_DECELERATION_TIME_UNIT = 0xF013, + + // Parameter P0-21 | Name : Frequency offset of auxiliary frequency source for X and Y operation + // Values : + // This parameter accepts a value from 0.00Hz to maximum frequency (P0-10). + E_SAKO_PARAM_P00_21_FREQUENCY_OFFSET_OF_AUXILIARY_FREQUENCY_SOURCE_FOR_X_AND_Y_OPERATION = 0xF015, + + // Parameter P0-22 | Name : Frequency reference resolution + // Values : + // 2 : 0.01Hz + E_SAKO_PARAM_P00_22_FREQUENCY_REFERENCE_RESOLUTION = 0xF016, + + // Parameter P0-23 | Name : Retentive of digital setting frequency upon power failure + // Values : + // 0 : Not retentive, + // 1 : Retentive + E_SAKO_PARAM_P00_23_RETENTIVE_OF_DIGITAL_SETTING_FREQUENCY_UPON_POWER_FAILURE = 0xF017, + + // Parameter P0-25 | Name : Acceleration/Deceleration time base frequency + // Values : + // 0 : Maximum frequency (P0-10), + // 1 : Set frequency, + // 2 : 100 Hz + E_SAKO_PARAM_P00_25_ACCELERATION_DECELERATION_TIME_BASE_FREQUENCY = 0xF019, + + // Parameter P0-26 | Name : Base frequency for UP/DOWN modification during running + // Values : + // 0 : Running frequency, + // 1 : Set frequency + E_SAKO_PARAM_P00_26_BASE_FREQUENCY_FOR_UP_DOWN_MODIFICATION_DURING_RUNNING = 0xF01A, + + // Parameter P0-27 | Name : Binding command source to frequency source + // Values : + // Unit's digit (Binding operation panel command to frequency source) + // 0 : No binding + // 1 : Frequency source by digital setting + // 2 : AI1 + // 3 : AI2 + // 4 : Panel potentiometer external keyboard potentiometer + // 5 : HDI Pulse setting (DI5) + // 6 : Multi-command + // 7 : Simple PLC + // 8 : PID + // 9 : Communication setting + // Ten's digit (Binding terminal command to frequency source) - (Refer to manual for specific values) + // Hundred's digit (Binding communication command to frequency source) - (Refer to manual for specific values) + E_SAKO_PARAM_P00_27_BINDING_COMMAND_SOURCE_TO_FREQUENCY_SOURCE = 0xF01B, + + //////////////////////////////////////////////////////////////////////////// + // P1-00 Motor Parameters + //////////////////////////////////////////////////////////////////////////// + + // Parameter P1-00 | Name : Motor type selection + // Values : + // 0 : Common asynchronous motor + // 2 : Permanent magnetic synchronous motor + E_SAKO_PARAM_P1_00_MOTOR_TYPE_SELECTION = 0xF100, + + // Parameter P1-01 | Name : Rated motor power + // Setting Range : 0.1kW ~ 1000.0kW + E_SAKO_PARAM_P1_01_RATED_MOTOR_POWER = 0xF101, + + // Parameter P1-02 | Name : Rated motor voltage + // Setting Range : 1V ~ 2000V + E_SAKO_PARAM_P1_02_RATED_MOTOR_VOLTAGE = 0xF102, + + // Parameter P1-03 | Name : Rated motor current + // Setting Range : 0.01A ~ 10.00A (AC drive power <= 2.2kW) + E_SAKO_PARAM_P1_03_RATED_MOTOR_CURRENT = 0xF103, + + // Parameter P1-04 | Name : Rated motor frequency + // Setting Range : 0.01Hz ~ maximum frequency + E_SAKO_PARAM_P1_04_RATED_MOTOR_FREQUENCY = 0xF104, + + // Parameter P1-05 | Name : Rated motor rotational speed + // Setting Range : 1rpm ~ 65535rpm + E_SAKO_PARAM_P1_05_RATED_MOTOR_ROTATIONAL_SPEED = 0xF105, + + // Parameter P1-10 | Name : No-load current (asynchronous motor) + // Setting Range : 0.01A ~ P1-03 + E_SAKO_PARAM_P1_10_NO_LOAD_CURRENT_ASYNCHRONOUS_MOTOR = 0xF110, + + // Parameter P1-37 | Name : Auto-tuning selection + // Values : + // 0 : No auto-tuning + // 1 : Asynchronous motor static auto-tuning + // 2 : Asynchronous motor complete auto-tuning + E_SAKO_PARAM_P1_37_AUTO_TUNING_SELECTION = 0xF137, + + //////////////////////////////////////////////////////////////////////////// + // P2-00 Motor Parameters + //////////////////////////////////////////////////////////////////////////// + + // Parameter P2-00 | Name : Speed loop proportional gain 1 + E_SAKO_PARAM_P02_00_SPEED_LOOP_PROPORTIONAL_GAIN_1 = 0xF200, + + // Parameter P2-01 | Name : Speed loop integral time 1 + E_SAKO_PARAM_P02_01_SPEED_LOOP_INTEGRAL_TIME_1 = 0xF201, + + // Parameter P2-02 | Name : Switchover frequency 1 + E_SAKO_PARAM_P02_02_SWITCHOVER_FREQUENCY_1 = 0xF202, + + // Parameter P2-03 | Name : Speed loop proportional gain 2 + E_SAKO_PARAM_P02_03_SPEED_LOOP_PROPORTIONAL_GAIN_2 = 0xF203, + + // Parameter P2-04 | Name : Speed loop integral time 2 + E_SAKO_PARAM_P02_04_SPEED_LOOP_INTEGRAL_TIME_2 = 0xF204, + + // Parameter P2-05 | Name : Switchover frequency 2 + E_SAKO_PARAM_P02_05_SWITCHOVER_FREQUENCY_2 = 0xF205, + + // Parameter P2-06 | Name : Vector control slip gain + E_SAKO_PARAM_P02_06_VECTOR_CONTROL_SLIP_GAIN = 0xF206, + + // Parameter P2-07 | Name : Time constant of speed loop filter + E_SAKO_PARAM_P02_07_TIME_CONSTANT_OF_SPEED_LOOP_FILTER = 0xF207, + + // Parameter P2-09 | Name : Torque upper limit source in speed control mode + // Values : + // 0 : Function code setting at P2-10 + // 1 : AI1 + // 2 : AI2 + // 3 : Panel potentiometer external keyboard potentiometer + // 4 : HDI Pulse setting + // 5 : Communication setting + // 6 : MIN(AI1, AI2) + // 7 : MAX(AI1, AI2) + // Note: The table also states "1-7 The full range of options corresponds to P2-10" under the P2-09 setting range. + E_SAKO_PARAM_P02_09_TORQUE_UPPER_LIMIT_SOURCE_IN_SPEED_CONTROL_MODE = 0xF209, + + // Parameter P2-10 | Name : Digital setting of torque upper limit in speed control mode + E_SAKO_PARAM_P02_10_DIGITAL_SETTING_OF_TORQUE_UPPER_LIMIT_IN_SPEED_CONTROL_MODE = 0xF210, + + // Parameter P2-13 | Name : Excitation adjustment proportional gain + E_SAKO_PARAM_P02_13_EXCITATION_ADJUSTMENT_PROPORTIONAL_GAIN = 0xF213, + + // Parameter P2-14 | Name : Excitation adjustment integral gain + E_SAKO_PARAM_P02_14_EXCITATION_ADJUSTMENT_INTEGRAL_GAIN = 0xF214, + + // Parameter P2-15 | Name : Torque adjustment proportional gain + E_SAKO_PARAM_P02_15_TORQUE_ADJUSTMENT_PROPORTIONAL_GAIN = 0xF215, + + // Parameter P2-16 | Name : Torque adjustment integral gain + E_SAKO_PARAM_P02_16_TORQUE_ADJUSTMENT_INTEGRAL_GAIN = 0xF216, + + // Parameter P2-17 | Name : Speed loop integral property + // Note : P2-17's unit digit sets integral separation. + // Values : + // 0 : Disabled + // 1 : Enabled + E_SAKO_PARAM_P02_17_SPEED_LOOP_INTEGRAL_PROPERTY = 0xF217, + + // Parameter P2-20 | Name : Maximum output voltage coefficient + E_SAKO_PARAM_P02_20_MAXIMUM_OUTPUT_VOLTAGE_COEFFICIENT = 0xF220, + + // Parameter P2-21 | Name : Maximum torque coefficient in weak magnetic field + E_SAKO_PARAM_P02_21_MAXIMUM_TORQUE_COEFFICIENT_IN_WEAK_MAGNETIC_FIELD = 0xF221, + + // Parameter P3-00 | Name : VF curve setting + // Values : + // 0 : Linear V/F + // 1 : Multi-point V/F + // 2 : Square V/F + // 3 : 1.2 power V/F + // 4 : 1.4 power V/F + // 6 : 1.6 power V/F + // 8 : 1.8 power V/F + // 9 : Reserved + // 10 : V/F complete separation + // 11 : V/F half separation + E_SAKO_PARAM_P03_00_VF_CURVE_SETTING = 0xF300, + + // Parameter P3-01 | Name : Torque boost + E_SAKO_PARAM_P03_01_TORQUE_BOOST = 0xF301, + + // Parameter P3-02 | Name : Cut-off frequency of torque boost + E_SAKO_PARAM_P03_02_CUT_OFF_FREQUENCY_OF_TORQUE_BOOST = 0xF302, + + // Parameter P3-03 | Name : Multi-point V/F frequency 1 + E_SAKO_PARAM_P03_03_MULTI_POINT_V_F_FREQUENCY_1 = 0xF303, + + // Parameter P3-04 | Name : Multi-point V/F voltage 1 + E_SAKO_PARAM_P03_04_MULTI_POINT_V_F_VOLTAGE_1 = 0xF304, + + // Parameter P3-05 | Name : Multi-point V/F frequency 2 + E_SAKO_PARAM_P03_05_MULTI_POINT_V_F_FREQUENCY_2 = 0xF305, + + // Parameter P3-06 | Name : Multi-point V/F voltage 2 + E_SAKO_PARAM_P03_06_MULTI_POINT_V_F_VOLTAGE_2 = 0xF306, + + // Parameter P3-07 | Name : Multi-point V/F frequency 3 + E_SAKO_PARAM_P03_07_MULTI_POINT_V_F_FREQUENCY_3 = 0xF307, + + // Parameter P3-08 | Name : Multi-point V/F voltage 3 + E_SAKO_PARAM_P03_08_MULTI_POINT_V_F_VOLTAGE_3 = 0xF308, + + // Parameter P3-09 | Name : V/F slip compensation gain + E_SAKO_PARAM_P03_09_V_F_SLIP_COMPENSATION_GAIN = 0xF309, + + // Parameter P3-10 | Name : VF over-excitation gain + E_SAKO_PARAM_P03_10_VF_OVER_EXCITATION_GAIN = 0xF310, + + // Parameter P3-11 | Name : VF oscillation suppression gain + E_SAKO_PARAM_P03_11_VF_OSCILLATION_SUPPRESSION_GAIN = 0xF311, + + //////////////////////////////////////////////////////////////////////////// + // P4-00 Motor Parameters + //////////////////////////////////////////////////////////////////////////// + // Parameter P4-00 | Name : DI1 terminal function selection + // Values : + // 0 : No function + // 1 : Forward RUN (FWD) or RUN + // 2 : Reverse RUN (REV) or RUN direction + // 3 : Three-line control + // 4 : Forward JOG (FJOG) + // 5 : Reverse JOG (RJOG) + // 6 : Terminal UP + // 7 : Terminal DOWN + // 8 : Coast to stop + // 9 : Fault reset (RESET) + E_SAKO_PARAM_P4_00_DI1_TERMINAL_FUNCTION_SELECTION = 0xF400, + + // Parameter P4-01 | Name : DI2 terminal function selection + // Values : + // 10 : RUN pause + // 11 : Normally open (NO) input of external fault + // 12 : Multi-reference terminal 1 + // 13 : Multi-reference terminal 2 + // 14 : Multi-reference terminal 3 + // 15 : Multi-reference terminal 4 + // 16 : Terminal 1 for acceleration/deceleration time selection + // 17 : Terminal 2 for acceleration/deceleration time selection + E_SAKO_PARAM_P4_01_DI2_TERMINAL_FUNCTION_SELECTION = 0xF401, + + // Parameter P4-02 | Name : DI3 terminal function selection + // Values : + // 18 : Frequency source switchover + // 19 : UP and DOWN setting clear (terminal, operation panel) + // 20 : Command source switchover terminal 1 + // 21 : Acceleration/Deceleration prohibited + E_SAKO_PARAM_P4_02_DI3_TERMINAL_FUNCTION_SELECTION = 0xF402, + + // Parameter P4-03 | Name : DI4 terminal function selection + // Values : + // 22 : PID pause + // 23 : PLC status reset + // 24 : Swing pause + // 25 : Counter input + // 26 : Counter reset + E_SAKO_PARAM_P4_03_DI4_TERMINAL_FUNCTION_SELECTION = 0xF403, + // Parameter P4-04 | Name : DI5 terminal function selection + // Values : + // 27 : Length count input + // 28 : Length reset + // 29 : Torque control prohibited + // 30 : Pulse input (enabled only for DI5) + // 31 : Reserved + // 32 : Immediate DC braking + // 33 : Normally closed (NC) input of external fault + // 34 : Frequency modification enable + // 35 : Reverse PID action direction + // 36 : External STOP terminal 1 + // 37 : Command source switchover terminal 2 + // 38 : PID integral pause + // 39 : Switchover between main frequency source X and preset frequency + // 40 : Switchover between auxiliary frequency source Y and preset frequency + // 41 : Reserved + // 42 : Reserved + // 43 : PID parameter switchover + // 44 : User-defined fault 1 + // 45 : User-defined fault 2 + // 46 : Speed control/Torque control switchover + // 47 : Emergency stop + // 48 : External STOP terminal 2 + // 49 : Deceleration DC braking + // 50 : Clear the current running time + // 51-59 : Reserved + // Default : 12 + E_SAKO_PARAM_P04_04_DI5_TERMINAL_FUNCTION_SELECTION = 0xF404, + + // Parameter P4-10 | Name : DI filter time + // Setting Range : 0.000s ~ 1.000s + // Default : 0.01s + E_SAKO_PARAM_P04_10_DI_FILTER_TIME = 0xF410, + + // Parameter P4-11 | Name : Terminal command mode + // Values : + // 0 : Two-line mode 1 + // 1 : Two-line mode 2 + // 2 : Three-line mode 1 + // Default : 0 + E_SAKO_PARAM_P04_11_TERMINAL_COMMAND_MODE = 0xF411, + + // Parameter P4-12 | Name : Terminal UP/DOWN rate + // Setting Range : 0.001Hz/s ~ 65.535Hz/s + // Default : 1.00Hz/s + E_SAKO_PARAM_P04_12_TERMINAL_UP_DOWN_RATE = 0xF412, + + // Parameter P4-13 | Name : Al curve 1 minimum input + // Setting Range : 0.00V ~ P4-15 + // Default : 0.00V + E_SAKO_PARAM_P04_13_AL_CURVE_1_MINIMUM_INPUT = 0xF413, + + // Parameter P4-14 | Name : Corresponding setting of Al curve 1 minimum input + // Setting Range : -100.0% ~ +100.0% + // Default : 0.0% + E_SAKO_PARAM_P04_14_CORRESPONDING_SETTING_OF_AL_CURVE_1_MINIMUM_INPUT = 0xF414, + + // Parameter P4-15 | Name : Al curve 1 maximum input + // Setting Range : P4-13 ~ +10.00V + // Default : 10.00V + E_SAKO_PARAM_P04_15_AL_CURVE_1_MAXIMUM_INPUT = 0xF415, + + // Parameter P4-16 | Name : Corresponding setting of Al curve 1 maximum input + // Setting Range : -100.0% ~ +100.0% + // Default : 100.0% + E_SAKO_PARAM_P04_16_CORRESPONDING_SETTING_OF_AL_CURVE_1_MAXIMUM_INPUT = 0xF416, + + // Parameter P4-17 | Name : All filter time + // Setting Range : 0.00s ~ 10.00s + // Default : 0.10s + // Note : "All" might be a typo for "Al1" or "AI1" based on P4-22 (Al2 filter time). + E_SAKO_PARAM_P04_17_ALL_FILTER_TIME = 0xF417, // Assuming "All" refers to AI1 based on context of P4-22. If it's a global AI filter, name might need adjustment. + + // Parameter P4-18 | Name : Al curve 2 minimum input + // Setting Range : 0.00V ~ P4-20 + // Default : 0.00V + E_SAKO_PARAM_P04_18_AL_CURVE_2_MINIMUM_INPUT = 0xF418, + + // Parameter P4-19 | Name : Corresponding setting of Al curve 2 minimum input + // Setting Range : -100.0% ~ +100.0% + // Default : 0.0% + E_SAKO_PARAM_P04_19_CORRESPONDING_SETTING_OF_AL_CURVE_2_MINIMUM_INPUT = 0xF419, + + // Parameter P4-20 | Name : Al curve 2 maximum input + // Setting Range : P4-18 ~ +10.00V + // Default : 10.00V + E_SAKO_PARAM_P04_20_AL_CURVE_2_MAXIMUM_INPUT = 0xF420, + + // Parameter P4-21 | Name : Corresponding setting of Al curve 2 maximum input + // Setting Range : -100.0% ~ +100.0% + // Default : 10.00V + // Note : Default value "10.00V" for a percentage range is likely a typo in the source document, expected a percentage like "100.0%". + E_SAKO_PARAM_P04_21_CORRESPONDING_SETTING_OF_AL_CURVE_2_MAXIMUM_INPUT = 0xF421, + + // Parameter P4-22 | Name : Al2 filter time + // Setting Range : 0.00s ~ 10.00s + // Default : 0.10s + E_SAKO_PARAM_P04_22_AL2_FILTER_TIME = 0xF422, + + // Parameter P4-23 | Name : Al curve 3 minimum input + // Setting Range : -10.00V ~ P4-25 + // Default : -10.00V + E_SAKO_PARAM_P04_23_AL_CURVE_3_MINIMUM_INPUT = 0xF423, + + // Parameter P4-24 | Name : Corresponding setting of Al curve 3 minimum input + // Setting Range : -100.0% ~ +100.0% + // Default : -100.0% + E_SAKO_PARAM_P04_24_CORRESPONDING_SETTING_OF_AL_CURVE_3_MINIMUM_INPUT = 0xF424, + + // Parameter P4-25 | Name : Al curve 3 maximum input + // Setting Range : P4-23 ~ +10.00V + // Default : 10.00V + E_SAKO_PARAM_P04_25_AL_CURVE_3_MAXIMUM_INPUT = 0xF425, + + // Parameter P4-26 | Name : Corresponding setting of Al curve 3 maximum input + // Setting Range : -100.0% ~ +100.0% + // Default : 100.0% + E_SAKO_PARAM_P04_26_CORRESPONDING_SETTING_OF_AL_CURVE_3_MAXIMUM_INPUT = 0xF426, + + // Parameter P4-27 | Name : Panel potentiometer filter time + // Setting Range : 0.00s ~ 10.00s + // Default : 0.10s + E_SAKO_PARAM_P04_27_PANEL_POTENTIOMETER_FILTER_TIME = 0xF427, + + // Parameter P4-28 | Name : HDI Pulse minimum input + // Setting Range : 0.00kHz ~ P4-30 + // Default : 0.00kHz + E_SAKO_PARAM_P04_28_HDI_PULSE_MINIMUM_INPUT = 0xF428, + + // Parameter P4-29 | Name : Corresponding setting of HDI minimum input + // Setting Range : -100.0% ~ 100.0% + // Default : 0.0% + E_SAKO_PARAM_P04_29_CORRESPONDING_SETTING_OF_HDI_MINIMUM_INPUT = 0xF429, + + // Parameter P4-30 | Name : HDI maximum input + // Setting Range : P4-28 ~ 100.00kHz + // Default : 50.00kHz + E_SAKO_PARAM_P04_30_HDI_MAXIMUM_INPUT = 0xF430, + + // Parameter P4-31 | Name : Corresponding setting of HDI pulse maximum input + // Setting Range : -100.0% ~ 100.0% + // Default : 100.0% + E_SAKO_PARAM_P04_31_CORRESPONDING_SETTING_OF_HDI_PULSE_MAXIMUM_INPUT = 0xF431, + + // Parameter P4-32 | Name : HDI filter time + // Setting Range : 0.00s ~ 10.00s + // Default : 0.10s + E_SAKO_PARAM_P04_32_HDI_FILTER_TIME = 0xF432, + + // Parameter P4-33 | Name : Al curve selection + // Values : + // The parameter value is an integer (e.g., H T U = Hundreds Tens Units). + // U (Unit's digit) : AI1 curve selection + // 1 : Curve 1 (2 points, see P4-13 to F4-16) + // 2 : Curve 2 (2 points, see P4-18 to F4-21) + // 3 : Curve 3 (2 points, see P4-23 to F4-26) + // 4 : Curve 4 (4 points, see A6-00 to A6-07) + // 5 : Curve 5 (4 points, see A6-08 to A6-15) + // T (Ten's digit) : AI2 curve selection (curve definitions are same as for AI1) + // 1 : Curve 1 + // 2 : Curve 2 + // 3 : Curve 3 + // 4 : Curve 4 + // 5 : Curve 5 + // H (Hundred's digit) : AI3 curve selection (curve definitions are same as for AI1) + // 1 : Curve 1 + // 2 : Curve 2 + // 3 : Curve 3 + // 4 : Curve 4 + // 5 : Curve 5 + // Default : 321 (AI3 uses Curve 3, AI2 uses Curve 2, AI1 uses Curve 1) + E_SAKO_PARAM_P04_33_AL_CURVE_SELECTION = 0xF433, + + // Parameter P4-34 | Name : Setting for AI less than minimum input + // Description : Defines behavior when analog input is below its configured minimum. + // Values : + // The parameter value is an integer (e.g., H T U = Hundreds Tens Units, representing AI3 AI2 AI1 settings). + // U (Unit's digit) : Setting for AI1 when input < minimum + // 0 : Output the minimum value of the AI1 curve + // 1 : Output 0.0% + // T (Ten's digit) : Setting for AI2 when input < minimum + // 0 : Output the minimum value of the AI2 curve + // 1 : Output 0.0% + // H (Hundred's digit) : Setting for AI3 when input < minimum + // 0 : Output the minimum value of the AI3 curve + // 1 : Output 0.0% + // Default : 000 (All AIs output minimum value of their respective curve if input is less than minimum) + E_SAKO_PARAM_P04_34_SETTING_FOR_AI_LESS_THAN_MINIMUM_INPUT = 0xF434, + + // Parameter P4-35 | Name : DI1 delay time + // Setting Range : 0.0s ~ 3600.0s + // Default : 0.0s + E_SAKO_PARAM_P04_35_DI1_DELAY_TIME = 0xF435, + + // Parameter P4-36 | Name : DI2 delay time + // Setting Range : 0.0s ~ 3600.0s + // Default : 0.0s + E_SAKO_PARAM_P04_36_DI2_DELAY_TIME = 0xF436, + + // Parameter P4-37 | Name : DI3 delay time + // Setting Range : 0.0s ~ 3600.0s + // Default : 0.0s + E_SAKO_PARAM_P04_37_DI3_DELAY_TIME = 0xF437, + + // Parameter P4-38 | Name : DI valid mode selection 1 + // Description : Configures the active logic level for Digital Inputs DI1 to DI5. + // Values : + // The parameter value is an integer (e.g., X4 X3 X2 X1 X0, where Xn is a digit for DI(n+1)). + // X0 (Unit's digit) : DI1 valid mode + // X1 (Ten's digit) : DI2 valid mode + // X2 (Hundred's digit) : DI3 valid mode + // X3 (Thousand's digit) : DI4 valid mode + // X4 (Ten thousand's digit) : DI5 valid mode + // For each digit (DIx valid mode): + // 0 : High level valid (active high) + // 1 : Low level valid (active low) + // Default : 00000 (DI5, DI4, DI3, DI2, DI1 are all High level valid) + E_SAKO_PARAM_P04_38_DI_VALID_MODE_SELECTION_1 = 0xF438, + + // Parameter P4-39 | Name : AI1 input voltage/current selection + // Values : + // 0 : Voltage input + // 1 : Current input + // Default : 0 + E_SAKO_PARAM_P04_39_AI1_INPUT_VOLTAGE_CURRENT_SELECTION = 0xF439, + + //////////////////////////////////////////////////////////////////////////// + // P5-00 Motor Parameters +// Parameter P5-00 | Name : FM terminal output mode + // Values : + // 0 : Pulse output (FMP) + // 1 : Switch signal output (FMR) + E_SAKO_PARAM_P05_00_FM_TERMINAL_OUTPUT_MODE = 0xF500, + + // Parameter P5-01 | Name : FMR output function selection + // Values : + // 0 : No output + // 1 : AC drive running + // 2 : Fault output (stop) + // 3 : Frequency-level detection FDT1 output + // 4 : Frequency reached + // 5 : Zero-speed running (no output at stop) + // 6 : Motor overload pre-warning + // 7 : AC drive overload pre-warning + // 8 : Set count value reached + // 9 : Designated count value reached + // 10 : Length reached + // 11 : PLC cycle complete + // 12 : Accumulative running time reached + // 13 : Frequency limited + // 14 : Torque limited + // 15 : Ready for RUN + // 16 : AI1 > AI2 + // 17 : Frequency upper limit reached + // 18 : Frequency lower limit reached (operation related) + // 19 : Undervoltage state output + // 20 : Communication setting + E_SAKO_PARAM_P05_01_FMR_OUTPUT_FUNCTION_SELECTION = 0xF501, + + // Parameter P5-02 | Name : Relay function (TA/TB/TC) + // Values : + // 21: Reserved + // 22: Reserved + // 23: Zero-speed running 2 (having output at stop) + // 24: Accumulative power-on time reached + // 25: Frequency level detection FDT2 output + // 26: Frequency 1 reached output + // 27: Frequency 2 reached output + // 28: Current 1 reached output + // 29: Current 2 reached output + // 30: Timing reached output + // 31: Al1 input limit exceeded + // 32: Load becoming 0 + // 33: Reverse running + // 34: Zero current state + // 35: IGBT temperature reached + // 36: Current limit exceeded + // 37: Frequency lower limit reached (having output at stop) + // 38: Alarm output + // 39: Motor overheat warning + // 40: Current running time reached + // 41: Fault output (There is no output if it is the coast to stop fault and undervoltage occurs.) + E_SAKO_PARAM_P05_02_RELAY_FUNCTION = 0xF502, + + // Parameter P5-06 | Name : FMP output function selection + // Values : + // 0 : Running frequency + // 1 : Set frequency + // 2 : Output current + // 3 : Output torque (absolute value) + // 4 : Output power + // 5 : Output voltage + // 6 : HDI input (100.0% corresponds 100.0kHz) + // 7 : AI1 + // 8 : AI2 + // 11 : Count value + // 12 : Communication setting + // 13 : Motor rotational speed + E_SAKO_PARAM_P05_06_FMP_OUTPUT_FUNCTION_SELECTION = 0xF506, + + // Parameter P5-07 | Name : AO1 output function selection + // Values : + // 0 : Running frequency + // 1 : Set frequency + // 2 : Output current + // 3 : Output torque (absolute value) + // 4 : Output power + // 5 : Output voltage + // 6 : HDI input (100.0% corresponds 100.0kHz) + // 7 : AI1 + // 8 : AI2 + // 11 : Count value + // 12 : Communication setting + // 13 : Motor rotational speed + // 14 : Output current (100.0% corresponds 1000.0A) + // 15 : Output voltage (100.0% corresponds 1000.0V) + // 16 : Output torque (actual value) + E_SAKO_PARAM_P05_07_AO1_OUTPUT_FUNCTION_SELECTION = 0xF507, + + // Parameter P5-09 | Name : Maximum FMP output frequency + // Range : 0.01kHz ~ 100.00kHz + E_SAKO_PARAM_P05_09_MAXIMUM_FMP_OUTPUT_FREQUENCY = 0xF509, + + // Parameter P5-10 | Name : AO1 offset coefficient + // Range : -100.0% ~ +100.0% + E_SAKO_PARAM_P05_10_AO1_OFFSET_COEFFICIENT = 0xF510, + + // Parameter P5-11 | Name : AO1 gain + // Range : -10.00 ~ +10.00 + E_SAKO_PARAM_P05_11_AO1_GAIN = 0xF511, + + // Parameter P5-17 | Name : FMR output delay time + // Range : 0.0s ~ 3600.0s + E_SAKO_PARAM_P05_17_FMR_OUTPUT_DELAY_TIME = 0xF517, + + // Parameter P5-18 | Name : Relay 1 output delay time + // Range : 0.0s ~ 3600.0s + E_SAKO_PARAM_P05_18_RELAY_1_OUTPUT_DELAY_TIME = 0xF518, + + // Parameter P5-19 | Name : Relay 2 output delay time + // Range : 0.0s ~ 3600.0s + E_SAKO_PARAM_P05_19_RELAY_2_OUTPUT_DELAY_TIME = 0xF519, + + //////////////////////////////////////////////////////////////////////////// + // P6-00 Motor Parameters + //////////////////////////////////////////////////////////////////////////// + + // P6 Start/Stop Control Parameters + // Parameter P6-00 | Name : Start mode + // Values : + // 0 : Direct start + // 1 : Rotational speed tracking restart + // 2 : Pre-excited start (asynchronous motor) + // Default : 0 + E_SAKO_PARAM_P6_00_START_MODE = 0xF600, + + // Parameter P6-01 | Name : Rotational speed tracking mode + // Values : + // 0 : From frequency at stop + // 1 : From power frequency + // 2 : From maximum frequency + // Default : 0 + E_SAKO_PARAM_P6_01_ROTATIONAL_SPEED_TRACKING_MODE = 0xF601, + + // Parameter P6-02 | Name : Rotational speed tracking speed + // Setting Range : 1 ~ 100 + // Default : 20 + E_SAKO_PARAM_P6_02_ROTATIONAL_SPEED_TRACKING_SPEED = 0xF602, + + // Parameter P6-03 | Name : Startup frequency + // Setting Range : 0.00Hz ~ 10.00Hz + // Default : 0.00Hz + E_SAKO_PARAM_P6_03_STARTUP_FREQUENCY = 0xF603, + + // Parameter P6-04 | Name : Startup frequency holding time + // Setting Range : 0.0s ~ 100.0s + // Default : 0.0s + E_SAKO_PARAM_P6_04_STARTUP_FREQUENCY_HOLDING_TIME = 0xF604, + + // Parameter P6-05 | Name : Startup DC braking current/ Pre-excited current + // Setting Range : 0% ~ 100% + // Default : 0% + E_SAKO_PARAM_P6_05_STARTUP_DC_BRAKING_CURRENT_PRE_EXCITED_CURRENT = 0xF605, + + // Parameter P6-06 | Name : Startup DC braking time/ Pre-excited time + // Setting Range : 0.0s ~ 100.0s + // Default : 0.0s + E_SAKO_PARAM_P6_06_STARTUP_DC_BRAKING_TIME_PRE_EXCITED_TIME = 0xF606, + + // Parameter P6-07 | Name : Acceleration/Deceleration mode + // Values : + // 0 : Linear acceleration/deceleration + // 1 : Static S-curve + // 2 : Dynamic S-curve + // Default : 0 + E_SAKO_PARAM_P6_07_ACCELERATION_DECELERATION_MODE = 0xF607, + + // Parameter P6-08 | Name : Time proportion of S-curve start segment + // Setting Range : 0.0% ~ (100%-P6-09) + // Default : 30.0% + E_SAKO_PARAM_P6_08_TIME_PROPORTION_OF_S_CURVE_START_SEGMENT = 0xF608, + + // Parameter P6-09 | Name : Time proportion of S-curve end segment + // Setting Range : 0.0% ~ (100%-P6-08) + // Default : 30.0% + E_SAKO_PARAM_P6_09_TIME_PROPORTION_OF_S_CURVE_END_SEGMENT = 0xF609, + + // Parameter P6-10 | Name : Stop mode + // Values : + // 0 : Decelerate to stop + // 1 : Coast to stop + // Default : 0 + E_SAKO_PARAM_P6_10_STOP_MODE = 0xF60A, + + // Parameter P6-11 | Name : Initial frequency of stop DC braking + // Setting Range : 0.00Hz ~ maximum frequency + // Default : 0.00Hz + E_SAKO_PARAM_P6_11_INITIAL_FREQUENCY_OF_STOP_DC_BRAKING = 0xF60B, + + // Parameter P6-12 | Name : Waiting time of stop DC braking + // Setting Range : 0.0s ~ 100.0s + // Default : 0.0s + E_SAKO_PARAM_P6_12_WAITING_TIME_OF_STOP_DC_BRAKING = 0xF60C, + + // Parameter P6-13 | Name : Stop DC braking current + // Setting Range : 0% ~ 100% + // Default : 0% + E_SAKO_PARAM_P6_13_STOP_DC_BRAKING_CURRENT = 0xF60D, + + // Parameter P6-14 | Name : Stop DC braking time + // Setting Range : 0.0s ~ 100.0s + // Default : 0.0s + E_SAKO_PARAM_P6_14_STOP_DC_BRAKING_TIME = 0xF60E, + + // Parameter P6-15 | Name : Brake use ratio + // Setting Range : 0% ~ 100% + // Default : 100% + E_SAKO_PARAM_P6_15_BRAKE_USE_RATIO = 0xF60F, + + //////////////////////////////////////////////////////////////////////////// + // P7-00 Motor Parameters + //////////////////////////////////////////////////////////////////////////// + + // Parameter P7-01 | Name : MF.K Key function selection + // Values : + // 0 : MF.K key disabled + // 1 : Switchover between operation panel control and remote command control (terminal or communication) + // 2 : Switchover between forward rotation and reverse rotation + // 3 : Forward JOG + // 4 : Reverse JOG + E_SAKO_PARAM_P07_01_MF_K_KEY_FUNCTION_SELECTION = 0xF701, + + // Parameter P7-02 | Name : STOP/RESET key function + // Values : + // 0 : STOP/RESET key enabled only in operation panel control + // 1 : STOP/RESET key enabled in any operation mode + E_SAKO_PARAM_P07_02_STOP_RESET_KEY_FUNCTION = 0xF702, + + // Parameter P7-03 | Name : LED display running parameters 1 + // Bitmask (0000-FFFF) : + // Bit00 : Running frequency 1 (Hz) + // Bit01 : Set frequency (Hz) + // Bit02 : Bus voltage (V) + // Bit03 : Output voltage (V) + // Bit04 : Output current (A) + // Bit05 : Output power (kW) + // Bit06 : Output torque (%) + // Bit07 : DI input status + // Bit08 : DO output status + // Bit09 : Al1 voltage (V) + // Bit10 : Al2 voltage (V) + // Bit11 : Panel potentiometer voltage (V) + // Bit12 : Count value + // Bit13 : Length value + // Bit14 : Load speed display + // Bit15 : PID setting + E_SAKO_PARAM_P07_03_LED_DISPLAY_RUNNING_PARAMETERS_1 = 0xF703, + + // Parameter P7-04 | Name : LED display running parameters 2 + // Bitmask (0000-FFFF) : + // Bit00 : PID feedback + // Bit01 : PLC stage + // Bit02 : HDI setting frequency (kHz) + // Bit03 : Running frequency 2 (Hz) + // Bit04 : Remaining running time + // Bit05 : Al1 voltage before correction (V) + // Bit06 : Al2 + // Bit07 : Panel potentiometer voltage before correction (V) + // Bit08 : Linear speed + // Bit09 : Current power-on time (Hour) + // Bit10 : Current running time (Min) + // Bit11 : HDI setting frequency (Hz) + // Bit12 : Communication setting value + // Bit13 : Encoder feedback speed (Hz) + // Bit14 : Main frequency X display (Hz) + // Bit15 : Auxiliary frequency Y display (Hz) + E_SAKO_PARAM_P07_04_LED_DISPLAY_RUNNING_PARAMETERS_2 = 0xF704, + + // Parameter P7-05 | Name : LED display stop parameters + // Bitmask (0000-FFFF) : + // Bit00 : Set frequency (Hz) + // Bit01 : Bus voltage (V) + // Bit02 : DI input status + // Bit03 : DO output status + // Bit04 : Al1 voltage (V) + // Bit05 : Al2 voltage (V) + // Bit06 : Potentiometer voltage (V) + // Bit07 : Count value + // Bit08 : Length value + // Bit09 : PLC stage + // Bit10 : Load speed + // Bit11 : PID setting + // Bit12 : HDI setting frequency (kHz) + E_SAKO_PARAM_P07_05_LED_DISPLAY_STOP_PARAMETERS = 0xF705, + + // Parameter P7-06 | Name : Load speed display coefficient + E_SAKO_PARAM_P07_06_LOAD_SPEED_DISPLAY_COEFFICIENT = 0xF706, + + // Parameter P7-07 | Name : Heatsink temperature of AC drive IGBT + E_SAKO_PARAM_P07_07_HEATSINK_TEMPERATURE_OF_AC_DRIVE_IGBT = 0xF707, + + // Parameter P7-09 | Name : Accumulative running time + E_SAKO_PARAM_P07_09_ACCUMULATIVE_RUNNING_TIME = 0xF709, + + // Parameter P7-12 | Name : Number of decimal places for load speed display + // Values : + // Unit' digit: U0-14 decimal number + // 0 : 0 decimal place + // 1 : 1 decimal place + // 2 : 2 decimal places + // 3 : 3 decimal places + // Ten' digit: U0-19/U0-29 decimal number + // 0 : 0 decimal place + // 1 : 1 decimal place + E_SAKO_PARAM_P07_12_NUMBER_OF_DECIMAL_PLACES_FOR_LOAD_SPEED_DISPLAY = 0xF712, + + // Parameter P7-13 | Name : Accumulative power-on time + E_SAKO_PARAM_P07_13_ACCUMULATIVE_POWER_ON_TIME = 0xF713, + + // Parameter P7-14 | Name : Accumulative power consumption + E_SAKO_PARAM_P07_14_ACCUMULATIVE_POWER_CONSUMPTION = 0xF714, +}; diff --git a/src/components/SakoTypes.h b/src/components/SakoTypes.h new file mode 100644 index 00000000..84efc360 --- /dev/null +++ b/src/components/SakoTypes.h @@ -0,0 +1,122 @@ +#pragma once + +// Sako Parameter Group Communication Access Addresses (from manual): +// P0 ~ PE Group: 0xF000 - 0xFEFF +// A0 ~ AC Group: 0xA000 - 0xACFF +// U0 Group (Monitoring): 0x7000 - 0x70FF + +enum class E_SAKO_STATUS : int +{ + E_SAKO_STATUS_DIRECTION = 0x3000 +}; +enum class E_SAKO_REGISTERS : int +{ + E_SAKO_REGISTERS_SET_DIR = 0x2000, + E_SAKO_REGISTERS_ERROR = 0x8000 +}; +enum class E_SAKO_DIRECTION : int +{ + E_SAKO_DIR_FWD = 1, + E_SAKO_DIR_REV = 2, + E_SAKO_DIR_JOGGING = 3, + E_SAKO_DIR_REVERSE_JOGGING = 4, + E_SAKO_DIR_FREE_STOP = 5, + E_SAKO_DIR_DECELERATION_STOP = 6, + E_SAKO_DIR_FAULT_RESET = 7 +}; + +enum class E_SAKO_MON : int { + E_SAKO_MON_RUNNING_FREQUENCY_HZ = 0x7000, // U0-00 Running frequency (Hz) + E_SAKO_MON_SET_FREQUENCY_HZ, // U0-01 Set frequency (Hz) + E_SAKO_MON_BUS_VOLTAGE_V, // U0-02 Bus voltage (V) + E_SAKO_MON_OUTPUT_VOLTAGE_V, // U0-03 Output voltage (V) + E_SAKO_MON_OUTPUT_CURRENT_A, // U0-04 Output current (A) + E_SAKO_MON_OUTPUT_POWER_KW, // U0-05 Output power (kW) + E_SAKO_MON_OUTPUT_TORQUE_PERCENT, // U0-06 Output torque (%) + E_SAKO_MON_DI_INPUT_STATE, // U0-07 DI input state + E_SAKO_MON_DO_OUTPUT_STATE, // U0-08 DO output state + E_SAKO_MON_AI1_VOLTAGE_V, // U0-09 AI1 voltage (V) + E_SAKO_MON_AI2_VOLTAGE_V_CURRENT_MA, // U0-10 AI2 voltage (V)/current (mA) + E_SAKO_MON_PANEL_POTENTIOMETER_VOLTAGE_V, // U0-11 Panel potentiometer voltage (V) + E_SAKO_MON_COUNT_VALUE, // U0-12 Count value + E_SAKO_MON_LENGTH_VALUE, // U0-13 Length value + E_SAKO_MON_LOAD_SPEED_DISPLAY, // U0-14 Load speed display + E_SAKO_MON_PID_SETTING, // U0-15 PID setting + E_SAKO_MON_PID_FEEDBACK, // U0-16 PID feedback + E_SAKO_MON_PLC_STAGE, // U0-17 PLC stage + E_SAKO_MON_HDI_INPUT_PULSE_FREQUENCY_HZ, // U0-18 HDI input pulse frequency (Hz) + E_SAKO_MON_FEEDBACK_SPEED_HZ, // U0-19 Feedback speed (Hz) + E_SAKO_MON_REMAINING_RUNNING_TIME, // U0-20 Remaining running time + E_SAKO_MON_AI1_VOLTAGE_BEFORE_CORRECTION, // U0-21 AI1 voltage before correction + E_SAKO_MON_AI2_VOLTAGE_V_CURRENT_MA_BEFORE_CORRECTION, // U0-22 AI2 voltage (V)/current (mA) before correction + E_SAKO_MON_PANEL_POTENTIOMETER_VOLTAGE_BEFORE_CORRECTION, // U0-23 Panel potentiometer voltage before correction + E_SAKO_MON_LINEAR_SPEED, // U0-24 Linear speed + E_SAKO_MON_ACCUMULATIVE_POWER_ON_TIME, // U0-25 Accumulative power-on time + E_SAKO_MON_ACCUMULATIVE_RUNNING_TIME, // U0-26 Accumulative running time + E_SAKO_MON_HDI_PULSE_INPUT_FREQUENCY, // U0-27 HDI pulse input frequency + E_SAKO_MON_COMMUNICATION_SETTING_VALUE, // U0-28 Communication setting value + + E_SAKO_MON_MAIN_FREQUENCY_X = 1030, // U0-30 Main frequency X + E_SAKO_MON_AUXILIARY_FREQUENCY_Y, // U0-31 Auxiliary frequency Y + E_SAKO_MON_VIEWING_ANY_REGISTER_ADDRESS_VALUE, // U0-32 Viewing any register address value + + E_SAKO_MON_TARGET_TORQUE_PERCENT = 1035, // U0-35 Target torque (%) + E_SAKO_MON_ROTATION_POSITION, // U0-36 Rotation position + E_SAKO_MON_POWER_FACTOR_ANGLE, // U0-37 Power factor angle + + E_SAKO_MON_TARGET_VOLTAGE_UPON_VF_SEPARATION = 1039, // U0-39 Target voltage upon V/F separation + E_SAKO_MON_OUTPUT_VOLTAGE_UPON_VF_SEPARATION, // U0-40 Output voltage upon V/F separation + E_SAKO_MON_DI_STATE_VISUAL_DISPLAY, // U0-41 DI state visual display + E_SAKO_MON_DO_STATE_VISUAL_DISPLAY, // U0-42 DO state visual display + E_SAKO_MON_DI_FUNCTION_STATE_VISUAL_DISPLAY_1_FUNCTION_01_40, // U0-43 DI function state visual display 1 (function 01-40) + E_SAKO_MON_DI_FUNCTION_STATE_VISUAL_DISPLAY_2_FUNCTION_41_80, // U0-44 DI function state visual display 2 (function 41-80) + E_SAKO_MON_FAULT_INFORMATION, // U0-45 Fault information + + E_SAKO_MON_CURRENT_SET_FREQUENCY_PERCENT = 1059, // U0-59 Current set frequency(%) + E_SAKO_MON_CURRENT_RUNNING_FREQUENCY_PERCENT, // U0-60 Current running frequency(%) + E_SAKO_MON_AC_DRIVE_RUNNING_STATE, // U0-61 AC drive running state + E_SAKO_MON_CURRENT_FAULT_CODE, // U0-62 Current fault code + + E_SAKO_MON_TORQUE_UPPER_LIMIT = 1065, // U0-65 Torque upper limit +}; + +enum class E_SAKO_ERROR : int { + E_SAKO_ERROR_NO_FAULT = 0, + E_SAKO_ERROR_OVERCURRENT = 2, + E_SAKO_ERROR_OVERCURRENT_ACCEL = 3, + E_SAKO_ERROR_OVERCURRENT_DECEL = 4, + E_SAKO_ERROR_OVERVOLTAGE_CONST = 5, + E_SAKO_ERROR_OVERVOLTAGE_ACCEL = 6, + E_SAKO_ERROR_OVERVOLTAGE_DECEL = 7, + E_SAKO_ERROR_BUFFER_RESISTANCE_OVERLOAD = 8, + E_SAKO_ERROR_UNDERVOLTAGE = 9, + E_SAKO_ERROR_FREQUENCY_CONVERTER_OVERLOAD = 10, // 0x0A + E_SAKO_ERROR_MOTOR_OVERLOAD = 11, // 0x0B + E_SAKO_ERROR_INPUT_LACK_PHASE = 12, // 0x0C + E_SAKO_ERROR_OUTPUT_LACK_PHASE = 13, // 0x0D + E_SAKO_ERROR_MODULE_OVERHEATING = 14, // 0x0E + E_SAKO_ERROR_EXTERNAL_FAULT = 15, // 0x0F + E_SAKO_ERROR_COMMUNICATION_ABNORMAL = 16, // 0x10 + E_SAKO_ERROR_CONTACTOR_ABNORMAL = 17, // 0x11 + E_SAKO_ERROR_CURRENT_DETECTION = 18, // 0x12 + E_SAKO_ERROR_MOTOR_TUNING = 19, // 0x13 + E_SAKO_ERROR_ENCODER_PG_CARD_FAILURE = 20, // 0x14 + E_SAKO_ERROR_PARAMETER_RW_ABNORMAL = 21, // 0x15 - Corrected value + E_SAKO_ERROR_HARDWARE = 22, // 0x16 + E_SAKO_ERROR_MOTOR_SHORT_CIRCUIT_GROUND = 23, // 0x17 + E_SAKO_ERROR_RUN_TIME_UP = 26, // 0x1A + E_SAKO_ERROR_USER_CUSTOM_1 = 27, // 0x1B + E_SAKO_ERROR_USER_CUSTOM_2 = 28, // 0x1C + E_SAKO_ERROR_POWER_ON_TIME_UP = 29, // 0x1D + E_SAKO_ERROR_OFF_LOAD = 30, // 0x1E + E_SAKO_ERROR_LOSS_OF_PID_FEEDBACK = 31, // 0x1F + E_SAKO_ERROR_FAST_CURRENT_LIMIT_TIMEOUT = 40, // 0x28 + E_SAKO_ERROR_SWITCHING_MOTOR_FAULT = 41, // 0x29 + E_SAKO_ERROR_SPEED_DEVIATION_TOO_LARGE = 42, // 0x2A + E_SAKO_ERROR_MOTOR_OVERSPEED = 43, // 0x2B + E_SAKO_ERROR_MOTOR_OVERTEMPERATURE = 45, // 0x2D + E_SAKO_ERROR_ENCODER_LINE_NUMBER_SETTING = 90, // 0x5A + E_SAKO_ERROR_ENCODER_UNCONNECTED = 91, // 0x5B + E_SAKO_ERROR_INITIAL_POSITION = 92, // 0x5C + E_SAKO_ERROR_SPEED_FEEDBACK = 94, // 0x5E +}; \ No newline at end of file diff --git a/src/components/StatusLight.h b/src/components/StatusLight.h new file mode 100644 index 00000000..c7ab70ed --- /dev/null +++ b/src/components/StatusLight.h @@ -0,0 +1,201 @@ +#ifndef STATUS_LIGHT_H +#define STATUS_LIGHT_H + +#include +#include +#include +#include +#include +#include +#include +#include "config.h" +#include "config-modbus.h" +#include "modbus/ModbusTCP.h" + +// class ModbusTCP; // Removed forward declaration + +enum STATUS_LIGHT_STATE { + OFF = 0, + ON = 1, + BLINK = 2 +}; + +class StatusLight : public Component +{ +private: + short modbusAddress; + +public: + // --- Moved Member Variables Here --- + short pin; + millis_t status_blink_TS; + bool doBlink; + bool last_blink; + millis_t blink_start_ts; + millis_t max_blink_time; + STATUS_LIGHT_STATE state; + // --- End Moved Member Variables --- + + StatusLight(Component *owner, short _ledPin, short _key, short _addr) + : last_blink(0), + status_blink_TS(0), + doBlink(false), + pin(_ledPin), + modbusAddress(_addr), + Component(String("STATUS_LIGHT_") + _key, _key, Component::COMPONENT_DEFAULT, owner), + state(STATUS_LIGHT_STATE::OFF) + { + doBlink = false; + status_blink_TS = 0; + last_blink = !digitalRead(pin); + + // Determine address based on key and set capability + short determinedAddress = -1; + switch (_key) { + case COMPONENT_KEY_FEEDBACK_0: determinedAddress = MB_MONITORING_STATUS_FEEDBACK_0; break; + case COMPONENT_KEY_FEEDBACK_1: determinedAddress = MB_MONITORING_STATUS_FEEDBACK_1; break; + // Add other cases if needed + } + + if (determinedAddress != -1) { + modbusAddress = determinedAddress; // Assign determined address + setNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS); + } else { + Log.errorln("StatusLight Constructor: Cannot determine Modbus address for unknown key: %d", _key); + } + } + + void setBlink(bool blink) + { + // Uses the current 'state' as the base for blinking or steady state. + status_blink(blink); + } + + void on() + { + set(1); + } + + void off() + { + set(0); + } + + short loop() override + { + if (doBlink) + { + unsigned long currentMillis = millis(); + if (currentMillis - status_blink_TS >= STATUS_BLINK_INTERVAL) + { + last_blink = !last_blink; + digitalWrite(pin, last_blink); + status_blink_TS = currentMillis; + } + } + else + { + // Maybe set pin based on 'state' if not blinking? + // digitalWrite(pin, (state == ON) ? HIGH : LOW); // Example + } + + return E_OK; + } + + short set(short val0, short val1 = 0) // val0: base state (0=OFF, 1=ON), val1: blink control (0=disable, 1=enable) + { + // 1. Update the internal base state based on val0 + state = (val0 != 0) ? STATUS_LIGHT_STATE::ON : STATUS_LIGHT_STATE::OFF; + + // 2. Set blinking status based on val1 + // status_blink will handle setting the pin if blinking is stopped. + status_blink(val1 != 0); + + // 3. If not blinking, ensure the pin reflects the 'state' and notify. + if (!doBlink) { + // This explicit digitalWrite is necessary if blinking was already off + // and val1 keeps it off, as status_blink might not have transitioned. + digitalWrite(pin, (state == STATUS_LIGHT_STATE::ON) ? HIGH : LOW); + if (hasNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS)) { + notifyStateChange(); + } + } + + return E_OK; + } + + short setup() + { + return E_OK; + } + + short debug() + { + return info(); + } + short info() override + { + Log.verboseln("StatusLight::info - Key=%d | Pin=%d | State=%d | Value=%d", id, pin, (int)state, digitalRead(pin)); + return E_OK; + } + + void status_blink(bool newBlinkState) + { + bool oldBlinkState = doBlink; + + if (!oldBlinkState && newBlinkState) // Transition: OFF -> ON (Starting to blink) + { + blink_start_ts = millis(); // Record when blinking period starts + // status_blink_TS for individual blinks is handled by loop() + } + + doBlink = newBlinkState; // Apply the new blink state + + if (oldBlinkState && !newBlinkState) // Transition: ON -> OFF (Stopping blinking) + { + // Set the pin to reflect the current underlying 'state' + digitalWrite(pin, (state == STATUS_LIGHT_STATE::ON) ? HIGH : LOW); + } + } + + short serial_register(Bridge *bridge) + { + bridge->registerMemberFunction(id, this, C_STR("set"), (ComponentFnPtr)&StatusLight::set); + bridge->registerMemberFunction(id, this, C_STR("info"), (ComponentFnPtr)&StatusLight::info); + return E_OK; + } + + // --- Network Interface --- + // StatusLight likely only needs read access via Modbus? + + short mb_tcp_read(short address) override { + if (hasNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS) && address == modbusAddress) { + // Return 1 if ON or BLINKING, 0 if OFF? + // Or return state enum value? + // Let's return 1 if pin is currently HIGH (covers ON and blinking phases) + return digitalRead(pin) == HIGH ? 1 : 0; + } + return 0; // Default for wrong address or capability disabled + } + + // mb_tcp_write might not be needed, or could control state/blink? + short mb_tcp_write(short address, short value) override { + if (hasNetCapability(OBJECT_NET_CAPS::E_NCAPS_MODBUS) && address == modbusAddress) { + status_blink(false); // Stop blinking if written externally + set(value); // Use internal set method + return E_OK; + } + return E_INVALID_PARAMETER; // Wrong address or capability disabled + } + + // --- Member variables were moved above --- + // short pin; + // millis_t status_blink_TS; + // bool doBlink; + // bool last_blink; + // millis_t blink_start_ts; + // millis_t max_blink_time; + // STATUS_LIGHT_STATE state; +}; + +#endif diff --git a/src/components/StepperController.h b/src/components/StepperController.h new file mode 100644 index 00000000..441156c1 --- /dev/null +++ b/src/components/StepperController.h @@ -0,0 +1,174 @@ +#ifndef STEPPERCONTROLLER_H +#define STEPPERCONTROLLER_H + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "../features.h" +#include "../enums.h" +#include "../config.h" + +class StepperController : public Component, public ModbusValue +{ + +public: + enum MOTOR_STATUS + { + MOTOR_RUNNING, + MOTOR_IDLE, + MOTOR_OVERLOAD, + MOTOR_ERROR, + MOTOR_UNKNOWN + }; + + struct StepperControllerParams + { + Component *owner; + short dirPin; + short pulsePin; + short feedbackPin; + short overloadPin; + short enabled; + short speed; + short pulseWidth; + short dir; + short id; + short addressStart; + }; + + StepperController( + Component *owner, + short dirPin, + short pulsePin, + short feedbackPin, + short overloadPin, + short enabled, + short speed, + short pulseWidth, + short dir, + short id, + short addressStart) : ModbusValue(addressStart), + Component("Stepper", id, Component::COMPONENT_DEFAULT, owner), + stepper(AccelStepper::DRIVER, pulsePin, dirPin), + _speed(speed), + _pulseWidth(pulseWidth), + _feedback(feedbackPin), + _overload(overloadPin), + _enabled(enabled), + _dir(dir), + _dirPin(dirPin), + _pulsePin(pulsePin), + _addressStart(addressStart), + _status(MOTOR_STATUS::MOTOR_RUNNING) + { + SBI(nFlags, OBJECT_NET_CAPS::E_NCAPS_MODBUS); + } + + short speed(short val0, short val1 = 0) + { + _speed = val0; + int range[STEPPER_MODUBUS_RANGE] = {_speed, _dir, _status, 0}; + onSet(range, STEPPER_MODUBUS_RANGE); + return E_OK; + } + short getSpeed(){ + return _speed; + } + short dir(short val0, short val1) + { + _dir = val0; + return E_OK; + } + short setup() + { + setRegisterMode(MB_REGISTER_MODE::E_MB_REGISTER_MODE_READ_WRITE); + setNumberAddresses(STEPPER_MODUBUS_RANGE); + setFunctionCode(MB_FC::MB_FC_READ_REGISTERS); + setAddress(_addressStart); + stepper.setMaxSpeed(STEPPER_MAX_SPEED_0); + stepper.setMinPulseWidth(_pulseWidth); + stepper.setPinsInverted(_dir, false, false); + + int range[STEPPER_MODUBUS_RANGE] = {_speed, _dir, _status, analogRead(_overload)}; + onSet(range, STEPPER_MODUBUS_RANGE); + //Log.verboseln("stepper controller setup:%d", _addressStart); + //Log.verboseln("stepper controller dir:%d | pulse:%d | speed:%d", _dirPin,_pulsePin, _speed); + stepper.setSpeed(_speed); + return E_OK; + } + short pulseWidth(short val0, short val1) + { + _pulseWidth = val0; + return E_OK; + } + short debug(){ + Log.verboseln("StepperController::debug Speed=%d | Dir=%d | Address=%d | Status=%d | Load=%d", _speed, _dir, addr, _status, analogRead(_overload)); + return E_OK; + } + short info() + { + // Log.verboseln(F("StepperController::debug Speed=%d | Dir=%d | Address=%d | Status=%d" CR), _speed, _dir, addr, _status); + return E_OK; + } + short loop() + { + short netSpeed = clamp(netVal(addr), 0, STEPPER_MAX_SPEED_0); + short netDir = clamp(netVal(addr + MB_RW_STEPPER_DIR_OFFSET), 0, 1); + short netStatus = netVal(addr + MB_RW_STEPPER_STATUS_OFFSET); + short netUser = netVal(addr + MB_RW_STEPPER_USER_OFFSET); + if (netSpeed != _speed) + { + _speed = netSpeed; + } + if (netDir != _dir) + { + _dir = netDir; + } + int range[STEPPER_MODUBUS_RANGE] = {_speed, netDir, netSpeed, 0}; + onSet(range, STEPPER_MODUBUS_RANGE); + stepper.setPinsInverted(_dir, false, false); + stepper.setSpeed(_speed * 10); + stepper.runSpeed(); + return E_OK; + } + + short onRegisterMethods(Bridge *bridge) + { + // bridge->registerMemberFunction(id, this, C_STR("setFlag"), (ComponentFnPtr)&StepperController::setFlag); + // bridge->registerMemberFunction(id, this, C_STR("clearFlag"), (ComponentFnPtr)&StepperController::clearFlag); + // bridge->registerMemberFunction(id, this, C_STR("info"), (ComponentFnPtr)&StepperController::info); + /* + bridge->registerMemberFunction(id, this, C_STR("speed"), (ComponentFnPtr)&StepperController::speed); + bridge->registerMemberFunction(id, this, C_STR("pulseWidth"), (ComponentFnPtr)&StepperController::pulseWidth); + bridge->registerMemberFunction(id, this, C_STR("dir"), (ComponentFnPtr)&StepperController::dir); + */ + return E_OK; + } + + bool isOverloaded() + { + return analogRead(_overload) > STEPPER_OVERLOAD_THRESHOLD_0; + } + +private: + AccelStepper stepper; + short _speed; + short _dir; + short _dirPin; + short _pulsePin; + short _pulseWidth; + short _addressStart; + short _status; + short _feedback; + short _overload; + short _enabled; +}; + +#endif