diff --git a/.github/skills/update-readme-docs/SKILL.md b/.github/skills/update-readme-docs/SKILL.md new file mode 100644 index 0000000..c326d11 --- /dev/null +++ b/.github/skills/update-readme-docs/SKILL.md @@ -0,0 +1,27 @@ +--- +name: update-readme-docs +description: 'Update the plugin documentation sections in README.md (config example, supported types, join maps, feedbacks, methods). Use when: documenting a plugin, refreshing README after code changes, cleaning up generated README sections.' +--- + +# Update README plugin docs + +Regenerates the marker-delimited documentation sections in `README.md` on the current branch, then reviews them. + +## Procedure + +1. From the repo root, run `dotnet msbuild -t:UpdateReadmeDocs` (the `UpdateReadmeDocs` target in `src/Directory.Build.targets`, implemented in `build/ReadmeDocs.cs`). Requires only the .NET SDK. Do not switch branches. +2. Read `git diff README.md` and compare each generated section against the source in `src/`: + - **Minimum Essentials Framework Versions**: one entry per distinct `MinimumEssentialsFrameworkVersion` in the factories; do not edit by hand or mark it ``, or it stops following version bumps. + - **Supported Types**: must match every `TypeNames` entry in the plugin's factory classes (any file name). + - **Config Example**: `type` is set by the target to the first `TypeNames` entry of the first C# file (by file name) that sets `TypeNames`; do not change it by hand. The target also removes the unused `uid` property. Always regenerated: do not edit it by hand or mark it `` (the marker is ignored). Each property's value comes from the first `` block on that property that contains its JSON name; `pollTimeMs`, `warningTimeoutMs` and `errorTimeoutMs` default to `30000`, `180000` and `300000`. If a property still shows `SampleString`/`SampleValue`, add or fix the `` block on it in the config class (`MakeModelPropertiesConfig.cs` in the template), using the JSON name exactly, then run the target again. + - **Join Maps**: generated from classes deriving from `JoinMapBaseAdvanced`, whatever their file name. If a join is missing (for example a `JoinDataComplete` without `JoinNumber` or `JoinType`), write the table by hand from the definitions and mark the section ``. + - **Base Classes / Interfaces**: the target lists the base classes (Base Classes) and interfaces (Interfaces) declared by the plugin's device classes, excluding factories and join maps. Verify against `src/`; do not edit by hand unless you add ``. Interfaces uses the generator's `Interfaces Implemented` markers and is empty when there are none. + - **Public Methods / Feedbacks**: remove noise (non-public API, base classes of factories, template-only members) by hand and mark ``; for a section that does not apply, keep the markers with only `` between them (deleting the markers does not work, the generator re-adds them). +3. Ensure a blank line precedes each `` marker and the file ends with a newline. +4. Do not edit anything outside the ``/`` blocks. +5. Report which sections were regenerated, hand-edited (now ``), or removed. Do not commit unless asked. + +## Rules + +- `` inside a section makes the generator leave it untouched on later runs, except the Config Example, which is always regenerated. Use it only for sections you curated by hand, and say so in the summary. +- Never fabricate joins, feedbacks, or config properties; derive everything from `src/`. diff --git a/.github/workflows/essentialsplugins-updatereadme-caller.yml b/.github/workflows/essentialsplugins-updatereadme-caller.yml deleted file mode 100644 index 8f4bbe3..0000000 --- a/.github/workflows/essentialsplugins-updatereadme-caller.yml +++ /dev/null @@ -1,14 +0,0 @@ - -name: Generate README - -on: - push: - branches-ignore: - - 'robot-docs' - -jobs: - call-update-readme: - uses: PepperDash/workflow-templates/.github/workflows/update-readme.yml@main - with: - target-branch: ${{ github.ref_name }} - diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index e69de29..0000000 diff --git a/.husky/commit-msg b/.husky/commit-msg index c331c11..b2a4d6a 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -1,5 +1,5 @@ #!/bin/sh . "$(dirname "$0")/_/husky.sh" -## Lints the commit message against the Conventional Commits rules enforced in CI +## Lints the commit message against Conventional Commits (rules in .husky/csx/commit-lint.csx) dotnet husky run --group commit-msg --args "$1" diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 7ac839e..c03e748 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -5,7 +5,10 @@ "label": "Restore dotnet tools", "type": "process", "command": "dotnet", - "args": ["tool", "restore"], + "args": [ + "tool", + "restore" + ], "presentation": { "reveal": "silent", "close": true @@ -16,7 +19,10 @@ "label": "Install git hooks", "type": "process", "command": "dotnet", - "args": ["husky", "install"], + "args": [ + "husky", + "install" + ], "dependsOn": "Restore dotnet tools", "presentation": { "reveal": "silent", @@ -26,6 +32,17 @@ "runOn": "folderOpen" }, "problemMatcher": [] + }, + { + "label": "Update README docs", + "type": "process", + "command": "dotnet", + "args": [ + "msbuild", + "-nologo", + "-t:UpdateReadmeDocs" + ], + "problemMatcher": "$msCompile" } ] } diff --git a/LICENSE.md b/LICENSE.md index d67fed7..72f5269 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,4 +1,4 @@ -Copyright (c) <2020> PepperDash Technology Corporation +Copyright (c) 2020-2026 PepperDash Technology Corporation Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: diff --git a/README.md b/README.md index 2f148af..1953e9b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![PepperDash Essentials Pluign Logo](/images/essentials-plugin-blue.png) +![PepperDash Essentials Plugin Logo](/images/essentials-plugin-blue.png) ![PepperDash Essentials](https://img.shields.io/badge/PepperDash%20Essentials-≥%20v2.42.4-blue) ![.NET](https://img.shields.io/badge/.NET%20Framework-4.7.2-512BD4) @@ -7,97 +7,402 @@ # Essentials Plugin Template (c) 2026 -## License +Use this repository as the starting point for a new [PepperDash Essentials](https://github.com/PepperDash/Essentials) plugin. For background on plugins, see the Essentials Wiki [Plugins](https://pepperdash.github.io/Essentials/docs/Plugins.html) article. + +This README follows the [Diataxis](https://diataxis.fr/) structure: + +* [Tutorial](#tutorial) - build your first plugin from the template, step by step +* [How-to guides](#how-to-guides) - recipes for specific tasks +* [Reference](#reference) - files, build properties, targets, error codes and rules +* [Explanation](#explanation) - how the template and its automation work, and why +* [Plugin Documentation](#plugin-documentation) - the generated documentation of this plugin + +## Tutorial + +In this tutorial you create a plugin for a fictional serial/TCP display, the "Samsung MDC", from the template, build it, document it and commit it. It takes about 15 minutes. You need the [.NET SDK](https://dotnet.microsoft.com/download), Git, and Visual Studio or VS Code. + +### 1. Create the repository + +On GitHub, create a new repository from this template (**Use this template**), or fork it. Clone it and open the folder: + +``` +git clone https://github.com//epi-samsung-mdc.git +cd epi-samsung-mdc +``` + +### 2. Build it once + +``` +dotnet build +``` + +The build restores the Essentials NuGet packages, installs the git hooks, and writes two files to `output/`: `epi-make-model.4Series.1.0.0-local.cplz` (the plugin) and a `.nupkg` (the NuGet package). Run `git config core.hooksPath`; it prints `.husky`, which confirms the commit-message hook is active. + +### 3. Keep one device category + +The template has three example devices. A display is controlled over a serial or TCP connection, so keep `MakeModelDevice` and its factory, and delete the other two device classes and their factories: + +``` +git rm src/MakeModelLogicDevice.cs src/MakeModelLogicDeviceFactory.cs +git rm src/MakeModelCrestronDevice.cs src/MakeModelCrestronDeviceFactory.cs +``` + +### 4. Name the device + +1. Rename `MakeModelDevice` to `SamsungMdcDevice` and `MakeModelDeviceFactory` to `SamsungMdcDeviceFactory`, in both the class and the file name. Your editor's rename refactoring updates the references. +2. In `SamsungMdcDeviceFactory`, set the type name that configuration files will use: -Provided under MIT license + ```csharp + TypeNames = new List() { "samsungMdc" }; + ``` -## Overview +3. Build again with `dotnet build`. It succeeds, and the badges at the top of this README are unchanged because the Essentials version did not change. -Fork this repo when creating a new plugin for Essentials. For more information about plugins, refer to the Essentials Wiki [Plugins](https://pepperdash.github.io/Essentials/docs/Plugins.html) article. +### 5. Document the plugin -This repo contains example classes for the three main categories of devices: -* `MakeModelDevice`: Used for most third party devices which require communication over a streaming mechanism such as a Com port, TCP/SSh/UDP socket, CEC, etc -* `MakeModelLogicDevice`: Used for devices that contain logic, but don't require any communication with third parties outside the program -* `MakeModelCrestronDevice`: Used for devices that represent a piece of Crestron hardware +``` +dotnet msbuild -t:UpdateReadmeDocs +git diff README.md +``` + +The [Plugin Documentation](#plugin-documentation) sections now list `samsungMdc` under Supported Types, and only the base classes of the device you kept. + +### 6. Commit + +``` +git add -A +git commit -m "feat: add Samsung MDC display plugin" +``` + +The commit-msg hook checks the message and the commit succeeds. Try `git commit --allow-empty -m "added stuff"` to see it reject a message that does not follow the [commit message rules](#commit-message-rules). + +You now have a building, documented plugin. Next, work through [Rename the template](#rename-the-template) to finish customizing it, and [Release a version](#release-a-version) to publish it. + +## How-to guides + +### Rename the template -There are matching factory classes for each of the three categories of devices. The `MakeModelConfigObject` should be used as a template and modified for any of the categories of device. Same goes for the `MakeModeleBridgeJoinMap`. +Every name to replace is a form of "Make Model"; the [placeholder names](#placeholder-names) table lists each form and where it appears. Search the repository for `MakeModel`, `make-model`, `Make.Model` and `Make Model`, then: -This also illustrates how a plugin can contain multiple devices. +1. Rename the solution and project files (`epi-make-model.4Series.sln`, `src/epi-make-model.4Series.csproj`, and the project path inside the `.sln`), the namespace and the classes. The XML `remarks` and `example` tags on each class show the intended rename, for example `MakeModelDevice` to `SamsungMdcDevice`. +2. Delete the device categories and factories the plugin does not need. +3. In each factory, set `MinimumEssentialsFrameworkVersion` and `TypeNames`. +4. Update `MakeModelPropertiesConfig` and `MakeModelBridgeJoinMap` for the plugin's configuration and joins. +5. Update the [package properties](#package-properties). +6. Update `.releaserc.json` (see [Release a version](#release-a-version)). +7. [Regenerate the plugin documentation](#regenerate-the-plugin-documentation). -## Cloning Instructions +In Visual Studio, the Task List shows every remaining `TODO [ ]` item. -After forking this repository into your own GitHub space, you can create a new repository using this one as the template. Then you must install the necessary dependencies as indicated below. +### Change the minimum Essentials version -## Dependencies +1. Set `MinimumEssentialsFrameworkVersion` in each factory to the lowest Essentials version the plugin is tested against. +2. Set the `PepperDashEssentials` `PackageReference` version in the csproj to the same version or higher. +3. Run `dotnet build`. The `PepperDash Essentials` badge updates; commit `README.md` with the change. +4. [Regenerate the plugin documentation](#regenerate-the-plugin-documentation) so the Minimum Essentials Framework Versions section matches. -The [Essentials](https://github.com/PepperDash/Essentials) libraries are required. They referenced via nuget. You must have nuget.exe installed and in the `PATH` environment variable to use the following command. Nuget.exe is available at [nuget.org](https://dist.nuget.org/win-x86-commandline/latest/nuget.exe). +### Regenerate the plugin documentation -### Installing Dependencies +1. Run `dotnet msbuild -t:UpdateReadmeDocs` from the repo root, or in VS Code run **Terminal > Run Task... > Update README docs**. In Copilot Chat, `/update-readme-docs` runs the target and then reviews the result. +2. Review `git diff README.md`. If the Config Example shows placeholders such as `SampleString` or `SampleValue`, add an `` block for that property in the config class (see [Set the Config Example values](#set-the-config-example-values)) and run the target again. +3. Commit `README.md` with your changes. -Dependencies will be automatically installed when +### Edit a generated section by hand -### Instructions for Renaming Solution and Files +1. Edit the content between the section's `` and `` markers. +2. Add `` on the line after the `` marker so later runs leave the section alone. + +To hide a section that does not apply, keep its markers with only `` between them. Deleting the markers does not work; the generator adds them back. + +Do not mark Minimum Essentials Framework Versions as ``, or it stops following version changes. The Config Example is always regenerated and ignores ``; set its values in the config class instead. + +### Set the Config Example values + +The Config Example takes each property's value from the first `` block on that property whose `` contains the property's JSON name, for example: + +```csharp +/// +/// +/// "properties": { +/// "pollTimeMs": 60000 +/// } +/// +/// +[JsonProperty("pollTimeMs")] +public long PollTimeMs { get; set; } +``` + +Without an example, `pollTimeMs`, `warningTimeoutMs` and `errorTimeoutMs` default to `30000`, `180000` and `300000`, and other properties get placeholders from their C# type. Run `dotnet msbuild -t:UpdateReadmeDocs` after changing an example. + +### Check the README the way CI does + +``` +dotnet build -p:ReadmeMode=Check +``` -See the Task List in Visual Studio for a guide on how to start using the template. There is extensive inline documentation and examples as well. +The build fails with `PDREADME004` if the badges are stale and with `PDREADME005` if the plugin documentation is stale, and changes nothing. Fix it with `dotnet build` and `dotnet msbuild -t:UpdateReadmeDocs`. -For renaming instructions in particular, see the XML `remarks` tags on class definitions +### Release a version -## README Badges +1. Replace the placeholder pre-release entry in `.releaserc.json` (`replace-me-feature-branch`, `replace-me-prerelease`) with your pre-release branch and channel, or remove it if you only release from `main`. +2. Write [conventional commit messages](#commit-message-rules); they determine the next version. +3. Push. The build workflow releases when semantic-release finds a new version. Use the commit scope `force-patch` to force a patch release, or `no-release` to skip one. -The `.NET` and `PepperDash Essentials` badges at the top of this README are kept in sync by an MSBuild target in `src/Directory.Build.targets`, so no extra tools are needed: +### Install or skip the git hooks -* `.NET` badge - `TargetFramework` of the plugin project -* `PepperDash Essentials` badge - the highest `MinimumEssentialsFrameworkVersion` in the project's `*Factory.cs` files (subfolders included) +* The hooks install automatically on `dotnet restore` or `dotnet build`, and when VS Code runs the "Install git hooks" task on folder open. To install them manually: `dotnet tool restore`, then `dotnet husky install`. +* Skip the hook for one commit: `git commit --no-verify`. +* Skip the automatic install: set the environment variable `HUSKY=0`. -Every local `dotnet build` (or Visual Studio build) rewrites the badges when they are stale; commit the `README.md` change with your code. The `PepperDashEssentials` `PackageReference` version in the csproj must be equal to or greater than `MinimumEssentialsFrameworkVersion` (a prerelease such as `2.13.0-beta` counts as lower than `2.13.0`); otherwise the build fails with `PDREADME002`. +### Package properties -In CI (`CI=true`) the target only checks: a stale badge fails the build with `PDREADME004`, so a release is never packaged with an out-of-date README. +Set these plugin-specific properties in the csproj: -* Check locally without modifying files: `dotnet build -p:ReadmeMode=Check` -* Skip the target: `dotnet build -p:SkipReadmeBadges=true` +1. `PackageId` - the name used to install the package from NuGet +2. `PackageProjectUrl` - the plugin repository's URL +3. `AssemblyTitle` - the DLL name shown on a processor when the plugin is loaded +4. `Description` and `PackageTags` -Keep a `MinimumEssentialsFrameworkVersion = "x.y.z";` assignment in at least one factory, and keep the two badges in this README. Renaming the project or factory classes does not require changes. +Set `Product` and `RepositoryUrl` in `src/Directory.Build.props`. Shared values (`Version`, `Authors`, `Company`, `Copyright`, `PackageOutputPath`) are also defined there. -## Git Hooks (Husky.Net) +## Reference -This repo uses [Husky.Net](https://alirezanet.github.io/Husky.Net/) for one `commit-msg` hook. It runs `.husky/csx/commit-lint.csx`, which checks the message against [Conventional Commits](https://www.conventionalcommits.org/) using the type list from the shared `checkCommitMessage` workflow in PepperDash/workflow-templates (this template's CI does not currently run that check): +### Repository layout + +| Path | Contents | +| --- | --- | +| `src/MakeModelDevice.cs`, `src/MakeModelDeviceFactory.cs` | Device that talks to third-party equipment over a stream (serial, TCP/SSH/UDP, CEC) | +| `src/MakeModelLogicDevice.cs`, `src/MakeModelLogicDeviceFactory.cs` | Device with logic only, no external communication | +| `src/MakeModelCrestronDevice.cs`, `src/MakeModelCrestronDeviceFactory.cs` | Device that represents Crestron hardware | +| `src/MakeModelPropertiesConfig.cs` | Device configuration class (`MakeModelPropertiesConfig`): the device's `properties` in the configuration file | +| `src/MakeModelBridgeJoinMap.cs` | EISC bridge join map (`MakeModelBridgeJoinMap`) | +| `src/Directory.Build.props` | Shared version, package and copyright properties | +| `src/Directory.Build.targets` | CPLZ packaging, git hook install, README badge and docs targets | +| `build/ReadmeDocs.cs` | Plugin documentation generator, compiled by the build | +| `.husky/` | Commit-msg hook and its linter (`csx/commit-lint.csx`) | +| `.releaserc.json` | semantic-release configuration | +| `.github/workflows/EssentialsPlugins-builds-caller.yml` | Build and release workflow | +| `.github/skills/update-readme-docs/` | Copilot skill that regenerates and reviews the plugin documentation | + +### Placeholder names + +The template names everything after a fictional "Make Model" device. Replace each form with your plugin's make and model; for a Samsung MDC display, `MakeModel` becomes `SamsungMdc` and `epi-make-model` becomes `epi-samsung-mdc`. + +| Form | Where | +| --- | --- | +| `MakeModel` | Class and file names in `src/` (devices, factories, `MakeModelPropertiesConfig`, `MakeModelBridgeJoinMap`), and the namespace `PepperDash.Essentials.Plugins.MakeModel` (`RootNamespace` in the csproj and every `.cs` file) | +| `epi-make-model` | Solution and project file names, the GitHub repository name in `RepositoryUrl` (`src/Directory.Build.props`) and `PackageProjectUrl` (csproj) | +| `Make.Model` | `AssemblyTitle` and `PackageId` (both `PepperDash.Essentials.Plugins.Make.Model`) in the csproj | +| `Make Model` | `Product` (`src/Directory.Build.props`) and `Description` (csproj) | +| `examplePlugin…` | `TypeNames` in each factory, the configuration `type` values | + +### Build outputs + +| File | Description | +| --- | --- | +| `output/..cplz` | The plugin, for loading on a processor | +| `output/..nupkg` | The NuGet package, including `LICENSE.md` and this `README.md` | + +### Build targets + +| Target | Runs | Effect | +| --- | --- | --- | +| `UpdateReadmeBadges` | Before every local build | Rewrites stale `.NET` and `PepperDash Essentials` badges. Incremental. | +| `CheckReadmeBadges` | Before the build when `ReadmeMode` is `Check` | Fails on stale badges | +| `UpdateReadmeDocs` | Only when invoked (`dotnet msbuild -t:UpdateReadmeDocs`) | Regenerates the [Plugin Documentation](#plugin-documentation) sections | +| `CheckReadmeDocs` | Before the build when `ReadmeMode` is `Check` | Fails on stale plugin documentation | +| `InstallGitHooks` | Before restore, unless `CI=true` or `HUSKY=0` | Runs `dotnet tool restore` and `dotnet husky install` | + +The README targets run only for projects with `ProjectType` `ProgramLibrary`, and never in design-time builds. + +### Build properties + +| Property | Default | Effect | +| --- | --- | --- | +| `ReadmeMode` | `Check` when `CI=true`, otherwise `Update` | `Check` verifies the README without changing it | +| `SkipReadmeBadges` | not set | `true` skips the badge targets | +| `SkipReadmeDocsCheck` | not set | `true` skips `CheckReadmeDocs` | +| `ReadmePath` | `README.md` at the repo root | README file the targets update | +| `HUSKY` | not set | `0` skips the git hook install (environment variable) | + +### Error codes + +| Code | Meaning | Fix | +| --- | --- | --- | +| `PDREADME001` | No `MinimumEssentialsFrameworkVersion = "x.y.z";` assignment found in the project's C# files | Set it in at least one factory | +| `PDREADME002` | The `PepperDashEssentials` package version is lower than `MinimumEssentialsFrameworkVersion` (a prerelease such as `2.13.0-beta` counts as lower than `2.13.0`) | Raise the package version or lower the factory minimum | +| `PDREADME003` | A badge is missing from `README.md` | Restore the `.NET` and `PepperDash Essentials` badges at the top | +| `PDREADME004` | The badges are stale (check mode) | Run `dotnet build` and commit `README.md` | +| `PDREADME005` | The plugin documentation is stale (check mode) | Run `dotnet msbuild -t:UpdateReadmeDocs`, review and commit `README.md` | + +### Badges + +| Badge | Source | +| --- | --- | +| `.NET` | `TargetFramework` of the plugin project | +| `PepperDash Essentials` | The highest `MinimumEssentialsFrameworkVersion = "x.y.z";` assignment in the project's C# files, in any file and subfolder | + +### Generated documentation sections + +| Section | Source | +| --- | --- | +| Minimum Essentials Framework Versions | Each distinct `MinimumEssentialsFrameworkVersion` value | +| Config Example | The class whose name ends in `Config` or `ConfigObject` with the most properties. Values come from each property's `` block (see [Set the Config Example values](#set-the-config-example-values)). `type` is the first `TypeNames` entry of the first C# file (by file name) that sets `TypeNames`; the unused `uid` property is removed. Always regenerated, even with ``. | +| Supported Types | Every `TypeNames` entry | +| Join Maps | Classes deriving from `JoinMapBaseAdvanced`, in any file. Type (RW) is `R`, `W` or `R/W` from each join's `JoinCapabilities` | +| Base Classes, Interfaces | Base classes and interfaces declared by the plugin's own classes, excluding factories and join maps | +| Public Methods | Public methods in the project's C# files | +| Bool, Int and String Feedbacks | Public `BoolFeedback`, `IntFeedback` and `StringFeedback` members | + +A section containing `` is never changed, except the Config Example, which is always regenerated. Factories and join maps are found by their content, so renaming them (for example to `SonyBraviaDeviceFactory`) needs no changes. + +### Commit message rules * Header: `(): `, at most 100 characters * Types: `feat`, `fix`, `chore`, `docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `revert`, `wip` * A blank line between the header and the body -* Merge, `Revert "..."`, `fixup!`, `squash!` and `amend!` messages are allowed locally (squash `fixup!` commits before pushing) -* Breaking changes use a `BREAKING CHANGE:` footer; the `feat!:` form is not recognized by the release tooling +* Breaking changes: a `BREAKING CHANGE:` footer. The `feat!:` form is rejected because the release tooling does not recognize it. +* Allowed as-is: merge commits, `Revert "..."`, and `fixup!`, `squash!` and `amend!` commits (squash these before pushing) -The only requirement is the [.NET SDK](https://dotnet.microsoft.com/download). +The type list follows the shared `checkCommitMessage` workflow in PepperDash/workflow-templates. This template's build workflow does not run that check. -### Setup +### Requirements -The hook is installed automatically the first time you do any of the following in a fresh clone: +* [.NET SDK](https://dotnet.microsoft.com/download): building, the git hooks and all README automation. Nothing else is required. +* Visual Studio or VS Code +* The Essentials libraries come from the `PepperDashEssentials` NuGet package and restore automatically; `nuget.exe` is not required. -* Open the solution in Visual Studio, or run `dotnet restore` / `dotnet build` -* Open the folder in VS Code and allow the "Install git hooks" automatic task +## Explanation -To install it manually: +### Device categories +Essentials plugins usually wrap one of three kinds of device, so the template includes one example of each: a device that talks to third-party equipment over a stream (`MakeModelDevice`), a device that only contains logic (`MakeModelLogicDevice`), and a device that represents Crestron hardware (`MakeModelCrestronDevice`). Each has its own factory, which tells Essentials which configuration `type` values create it and which Essentials version it needs. One plugin can contain several devices. + +### Why the README is maintained by the build + +This README is packed into the plugin's NuGet package, so the badges and the plugin documentation must match the code that was released. Both are produced by MSBuild targets rather than by a git hook or a CI job: + +* A build target runs the same way in Visual Studio, VS Code, the command line and CI, and needs nothing beyond the .NET SDK. +* Git hooks can be skipped, and a CI job that commits to the repository creates extra commits after review. Generating locally means the change is reviewed with the code that caused it. +* CI runs the same targets in check mode, so a release cannot be packaged with a README that no longer matches the code. + +The badges are rewritten on every build because they are purely mechanical. The plugin documentation is regenerated only when you ask for it, because it needs a review: the Config Example can contain placeholders for properties without an `` block, and some sections are curated by hand with ``. + +CI checks the README only in the release build, which runs when semantic-release finds a new version. Ordinary pushes are not checked. + +### The documentation generator + +`build/ReadmeDocs.cs` is a C# port of `metadata.py` from [PepperDash/workflow-templates](https://github.com/PepperDash/workflow-templates), which the older `update-readme` workflow runs in CI. It produces the same sections, with these differences: the output order no longer depends on the file system, each Minimum Essentials Framework Version is listed once, a join map is also found when its file is not named after the class, interface names are matched case-sensitively, and Base Classes are listed before Interfaces as plain items. The two implementations are maintained separately. + +### Commit messages and versions + +Releases are versioned by [semantic-release](https://semantic-release.gitbook.io/), which reads the commit messages since the last release: `fix` produces a patch release, `feat` a minor release, and a `BREAKING CHANGE:` footer a major release. The commit-msg hook catches malformed messages before they reach the shared history, where they would be ignored or produce the wrong version. + +## License + +Provided under the MIT license; see [LICENSE.md](LICENSE.md). + +## Plugin Documentation + +The sections below are generated from the source code; see [Regenerate the plugin documentation](#regenerate-the-plugin-documentation) and [Generated documentation sections](#generated-documentation-sections). + + +### Minimum Essentials Framework Versions + +- 2.42.4 + + + +### Config Example + +```json +{ + "key": "device-1", + "name": "Example Device", + "type": "examplePluginCrestronDevice", + "group": "pluginDevices", + "properties": { + "control": { + "method": "tcpIp", + "tcpSshProperties": { + "address": "172.22.0.101", + "port": 23, + "username": "admin", + "password": "password", + "autoReconnect": true, + "autoReconnectIntervalMs": 10000 + } + }, + "pollTimeMs": 30000, + "warningTimeoutMs": 180000, + "errorTimeoutMs": 300000, + "DeviceDictionary": { + "item1": { + "name": "Item 1 Name", + "value": 1 + } + } + } +} ``` -dotnet tool restore -dotnet husky install -``` + + + +### Supported Types + +- examplePluginCrestronDevice +- examplePluginDevice +- examplePluginLogicDevice + + + +### Join Maps + +#### Digitals + +| Join | Type (RW) | Description | +| --- | --- | --- | +| 1 | R | Is Online | +| 2 | R/W | Connect (Held)/Disconnect (Release) & corresponding feedback | + +#### Analogs + +| Join | Type (RW) | Description | +| --- | --- | --- | +| 1 | R | Socket Status | + + + +### Base Classes + +- CrestronGenericBridgeableBaseDevice +- EssentialsBridgeableDevice + + + + + + +### Public Methods -Verify with `git config core.hooksPath`, which should print `.husky`. +- public void SendText(string text) +- public void SendBytes(byte[] bytes) +- public void Poll() + -### Usage + +### Bool Feedbacks -* Commit as usual; the hook runs on every `git commit`. -* Skip the hook for a single commit: `git commit --no-verify` -* Skip the automatic install on restore: set the `HUSKY=0` environment variable. It is also skipped when `CI=true`. +- ConnectFeedback +- OnlineFeedback + -## Build Instructions (PepperDash Internal) + +### Int Feedbacks -## Generating Nuget Package +- StatusFeedback + -A nuget package is automatically generated when the plugin is build. To modify the name and other details of the package, edit the following properties in the .csproj file: + -1. `PackageId` - This is the name that will be used to pull the package from Nuget once it's published -2. `PackgeProjectUrl` - This should match the URL for the plugin repo -3. `AssemblyTitle` - This is the dll file name that is will show on a processor when the plugin is loaded \ No newline at end of file + diff --git a/build/ReadmeDocs.cs b/build/ReadmeDocs.cs new file mode 100644 index 0000000..71cfc62 --- /dev/null +++ b/build/ReadmeDocs.cs @@ -0,0 +1,831 @@ +// README plugin documentation generator, compiled at build time by RoslynCodeTaskFactory +// (see the UpdateReadmeDocs / CheckReadmeDocs targets in src/Directory.Build.targets). +// +// Port of metadata.py from PepperDash/workflow-templates (commit 681f68e), followed by the template's +// post-processing (Config Example type/uid, Base Classes and Interfaces). Output matches metadata.py +// except that: +// - it is deterministic: source files are read in sorted folder order and each file's Supported Types +// keep their declared order (metadata.py used os.walk order and an unordered set) +// - each distinct Minimum Essentials Framework Version is listed once, including several in one file +// - the Config Example is always regenerated, even with ; a property's value comes from the +// first block on it that contains its JSON name (pollTimeMs, warningTimeoutMs and +// errorTimeoutMs default to 30000, 180000 and 300000), and key/name/group are realistic +// - join access (R, W or R/W) follows JoinCapabilities (metadata.py listed every join as R) +// - a join map is also found by its class declaration when its file is not named .cs +// - factories are recognized by their Essentials base class, whatever they are named +// - interfaces are matched case-sensitively (IpTableObjectBase is a base class) +// - Base Classes come before Interfaces, as plain list items +// +// Must stay compatible with netstandard2.0 / C# 7.3 so it also compiles under Visual Studio's MSBuild. + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using Microsoft.Build.Framework; +using Microsoft.Build.Utilities; + +public class GenerateReadmeDocs : Task +{ + /// README.md to update or check. + [Required] public string ReadmePath { get; set; } + + /// The project's own C# files; RelPath metadata (relative to the project) sets the read order. + [Required] public ITaskItem[] SourceFiles { get; set; } + + /// Fail when README.md is stale instead of rewriting it. + public bool CheckOnly { get; set; } + + private static readonly UTF8Encoding Utf8 = new UTF8Encoding(false); + + public override bool Execute() + { + var sources = SourceFiles + .Select(i => new SourceFile( + RelativePath(i), + ReadmeDocs.NormalizeNewlines(File.ReadAllText(i.GetMetadata("FullPath"), Utf8)))) + .ToList(); + sources.Sort((a, b) => ReadmeDocs.WalkOrder(a.RelPath, b.RelPath)); + + var original = File.ReadAllText(ReadmePath, Utf8); + var generated = ReadmeDocs.Generate(ReadmeDocs.NormalizeNewlines(original), sources, + message => Log.LogMessage(MessageImportance.Normal, "readme-docs: " + message)); + + if (string.Equals(generated, ReadmeDocs.NormalizeNewlines(original), StringComparison.Ordinal)) + { + Log.LogMessage(MessageImportance.High, "readme-docs: README.md up to date"); + return true; + } + if (CheckOnly) + { + Log.LogError(null, "PDREADME005", null, ReadmePath, 0, 0, 0, 0, + "README.md plugin documentation is stale; run 'dotnet msbuild -t:UpdateReadmeDocs' locally, review and commit README.md"); + return false; + } + + // Keep the line endings the README was checked out with + if (original.Contains("\r\n")) generated = generated.Replace("\n", "\r\n"); + File.WriteAllText(ReadmePath, generated, Utf8); + Log.LogMessage(MessageImportance.High, "readme-docs: README.md updated; review the diff, then commit it with your changes"); + return true; + } + + private static string RelativePath(ITaskItem item) + { + var rel = item.GetMetadata("RelPath"); + return string.IsNullOrEmpty(rel) ? item.ItemSpec : rel; + } +} + +public class SourceFile +{ + public SourceFile(string relPath, string content) + { + RelPath = relPath.Replace('\\', '/'); + Content = content; + } + + public string RelPath { get; private set; } + public string Content { get; private set; } + public string FileName { get { return RelPath.Substring(RelPath.LastIndexOf('/') + 1); } } +} + +public static class ReadmeDocs +{ + public static string NormalizeNewlines(string text) + { + return text.Replace("\r\n", "\n").Replace('\r', '\n'); + } + + /// os.walk order with sorted entries: a folder's files before its subfolders, each sorted ordinally. + public static int WalkOrder(string a, string b) + { + var pa = a.Replace('\\', '/').Split('/'); + var pb = b.Replace('\\', '/').Split('/'); + for (var i = 0; ; i++) + { + var aIsFile = i == pa.Length - 1; + var bIsFile = i == pb.Length - 1; + if (aIsFile != bIsFile) return aIsFile ? -1 : 1; + var c = string.CompareOrdinal(pa[i], pb[i]); + if (c != 0 || aIsFile) return c; + } + } + + public static string Generate(string readme, IList sources, Action log) + { + // ---- metadata.py ---- + var supportedTypes = new List(); + var minimumVersions = new List(); + var publicMethods = new List(); + var boolFeedbacks = new List(); + var intFeedbacks = new List(); + var stringFeedbacks = new List(); + foreach (var source in sources) + { + // De-duplicated per file only, as in metadata.py + supportedTypes.AddRange(ExtractSupportedTypes(source.Content).Distinct()); + // Listed once per distinct value (metadata.py took the first per file and repeated it for every factory) + foreach (Match version in MinimumVersionPattern.Matches(source.Content)) + if (!minimumVersions.Contains(version.Groups[1].Value)) minimumVersions.Add(version.Groups[1].Value); + foreach (Match m in PublicMethodPattern.Matches(source.Content)) publicMethods.Add(m.Value.Trim()); + var uncommented = LineComment.Replace(source.Content, ""); + AddFeedbacks(BoolFeedbackPattern, uncommented, boolFeedbacks); + AddFeedbacks(IntFeedbackPattern, uncommented, intFeedbacks); + AddFeedbacks(StringFeedbackPattern, uncommented, stringFeedbacks); + } + + var joins = new List(); + foreach (var joinMap in FindJoinMapClasses(sources)) + joins.AddRange(ParseJoinMap(joinMap, sources, log)); + + string configExample = ""; + var classDefs = ParseAllClasses(sources); + var configClasses = classDefs.Keys.Where(c => c.EndsWith("Config", StringComparison.Ordinal) || c.EndsWith("ConfigObject", StringComparison.Ordinal)).ToList(); + if (configClasses.Count == 0) + { + log("no config classes found"); + } + else + { + var main = configClasses[0]; + foreach (var c in configClasses) + if (classDefs[c].Count > classDefs[main].Count) main = c; + configExample = "### Config Example\n\n```json\n" + Json.Write(SampleConfig(main, classDefs, supportedTypes), 0) + "\n```\n"; + } + + readme = UpdateSection(readme, "Minimum Essentials Framework Versions", MarkdownList(minimumVersions, "Minimum Essentials Framework Versions")); + if (configExample.Length > 0) readme = UpdateSection(readme, "Config Example", configExample, honorSkip: false); + readme = UpdateSection(readme, "Supported Types", MarkdownList(supportedTypes, "Supported Types")); + readme = UpdateSection(readme, "Join Maps", JoinMapChart(joins)); + // Base Classes and Interfaces Implemented bodies are replaced by the post-processing below + readme = UpdateSection(readme, "Base Classes", ""); + readme = UpdateSection(readme, "Interfaces Implemented", ""); + readme = BaseClassesBeforeInterfaces(readme); + readme = UpdateSection(readme, "Public Methods", MarkdownList(publicMethods, "Public Methods")); + readme = UpdateSection(readme, "Bool Feedbacks", MarkdownList(boolFeedbacks, "Bool Feedbacks")); + readme = UpdateSection(readme, "Int Feedbacks", MarkdownList(intFeedbacks, "Int Feedbacks")); + readme = UpdateSection(readme, "String Feedbacks", MarkdownList(stringFeedbacks, "String Feedbacks")); + + // ---- template post-processing ---- + var byName = sources.OrderBy(s => s.FileName, StringComparer.OrdinalIgnoreCase).ToList(); + readme = FixConfigExample(readme, FindConfigType(byName)); + var baseTypes = new List(); + var interfaceTypes = new List(); + CollectDeclaredTypes(byName, baseTypes, interfaceTypes); + readme = SetSectionBody(readme, "Base Classes", DeclaredTypesBody("Base Classes", baseTypes)); + readme = SetSectionBody(readme, "Interfaces Implemented", DeclaredTypesBody("Interfaces", interfaceTypes)); + return readme; + } + + // ---------------------------------------------------------------- metadata.py extraction + + private static readonly Regex LineComment = new Regex("//.*"); + private static readonly Regex BlockComment = new Regex(@"/\*.*?\*/", RegexOptions.Singleline); + private static readonly Regex TypeNamesPattern = new Regex(@"TypeNames\s*=\s*new\s*List\(\)\s*\{([^}]+)\}"); + private static readonly Regex MinimumVersionPattern = new Regex(@"^\s*MinimumEssentialsFrameworkVersion\s*=\s*""([^""]+)""\s*;", RegexOptions.Multiline); + private static readonly Regex PublicMethodPattern = new Regex(@"public\s+\w+\s+\w+\s*\([^)]*\)\s*"); + private static readonly Regex BoolFeedbackPattern = FeedbackPattern("BoolFeedback"); + private static readonly Regex IntFeedbackPattern = FeedbackPattern("IntFeedback"); + private static readonly Regex StringFeedbackPattern = FeedbackPattern("StringFeedback"); + + private static Regex FeedbackPattern(string type) + { + return new Regex(@"public\s+" + type + @"\s+(\w+)(?:\s*\{[^}]*\}|\s*;|\s*=)"); + } + + private static IEnumerable ExtractSupportedTypes(string content) + { + foreach (Match m in TypeNamesPattern.Matches(LineComment.Replace(content, ""))) + foreach (var item in m.Groups[1].Value.Split(',')) + { + var type = item.Trim().Trim('"'); + if (type.Length > 0) yield return type; + } + } + + private static void AddFeedbacks(Regex pattern, string content, List target) + { + foreach (Match m in pattern.Matches(content)) + { + var name = m.Groups[1].Value.Trim(); + if (name.Length > 0) target.Add(name); + } + } + + private static string MarkdownList(List items, string title) + { + if (items.Count == 0) return ""; + var sb = new StringBuilder("### " + title + "\n\n"); + foreach (var item in items) sb.Append("- ").Append(item).Append('\n'); + return sb.Append('\n').ToString(); + } + + // ---------------------------------------------------------------- join maps + + private static readonly Regex ClassWithBasesPattern = new Regex( + @"^\s*(?:\[[^\]]+\]\s*)*(?:public\s+|private\s+|protected\s+)?(?:partial\s+)?class\s+([A-Za-z_]\w*)(?:\s*:\s*([^\{]+))?\s*\{", + RegexOptions.Multiline); + + private static readonly Regex JoinPattern = new Regex( + @"\[JoinName\(""(?[^""]+)""\)\]\s*" + + @"public\s+JoinDataComplete\s+(?\w+)\s*=\s*" + + @"new\s+JoinDataComplete\s*\(\s*" + + @"(?.*?)\)\s*;", + RegexOptions.Singleline); + + private static readonly Regex JoinDataPattern = new Regex(@"new\s+JoinData\s*(?:\(\s*\))?\s*\{(.*?)\}", RegexOptions.Singleline); + private static readonly Regex JoinMetadataPattern = new Regex(@"new\s+JoinMetadata\s*(?:\(\s*\))?\s*\{(.*?)\}", RegexOptions.Singleline); + + private class JoinInfo + { + public string Number; + public string Type; + public string Description; + public string Access; + } + + private static List FindJoinMapClasses(IList sources) + { + // Python dict semantics: first-seen order, last definition wins + var order = new List(); + var bases = new Dictionary>(); + foreach (var source in sources) + foreach (Match m in ClassWithBasesPattern.Matches(source.Content)) + { + var name = m.Groups[1].Value; + if (!bases.ContainsKey(name)) order.Add(name); + bases[name] = m.Groups[2].Success + ? m.Groups[2].Value.Split(',').Select(b => b.Trim()).ToList() + : new List(); + } + return order.Where(n => bases[n].Contains("JoinMapBaseAdvanced")).ToList(); + } + + private static IEnumerable ParseJoinMap(string className, IList sources, Action log) + { + // metadata.py reads the join map from .cs; fall back to the file that declares the + // class, so a join map still works when its file is not renamed along with the class + var declares = new Regex(@"\bclass\s+" + Regex.Escape(className) + @"\b"); + var file = sources.FirstOrDefault(s => s.FileName == className + ".cs") + ?? sources.FirstOrDefault(s => declares.IsMatch(s.Content)); + if (file == null) + { + log("join map file not found: " + className + ".cs; skipping"); + yield break; + } + + var content = BlockComment.Replace(LineComment.Replace(file.Content, ""), ""); + foreach (Match match in JoinPattern.Matches(content)) + { + var joinName = match.Groups["join_name"].Value; + var joinParams = match.Groups["join_params"].Value; + string number = null, description = null, type = null, access = "R"; + + var data = JoinDataPattern.Match(joinParams); + if (data.Success) + { + var m = Regex.Match(data.Groups[1].Value, @"JoinNumber\s*=\s*(\d+)"); + if (m.Success) number = m.Groups[1].Value; + } + var metadata = JoinMetadataPattern.Match(joinParams); + if (metadata.Success) + { + var d = Regex.Match(metadata.Groups[1].Value, @"Description\s*=\s*""([^""]+)"""); + if (d.Success) description = d.Groups[1].Value; + var t = Regex.Match(metadata.Groups[1].Value, @"JoinType\s*=\s*eJoinType\.(\w+)"); + if (t.Success) type = t.Groups[1].Value; + var c = Regex.Match(metadata.Groups[1].Value, @"JoinCapabilities\s*=\s*([^,\r\n}]+)"); + if (c.Success) access = JoinAccess(c.Groups[1].Value) ?? access; + } + + if (joinName.Length > 0 && number != null && type != null) + yield return new JoinInfo { Number = number, Type = type, Description = description, Access = access }; + else + log("incomplete join information for '" + joinName + "'; skipping"); + } + } + + // ToSIMPL is feedback SIMPL reads (R), FromSIMPL is a command SIMPL writes (W); null for None + private static string JoinAccess(string capabilities) + { + var read = Regex.IsMatch(capabilities, @"\b(?:ToSIMPL|ToFromSIMPL)\b"); + var write = Regex.IsMatch(capabilities, @"\b(?:FromSIMPL|ToFromSIMPL)\b"); + if (read && write) return "R/W"; + if (read) return "R"; + if (write) return "W"; + return null; + } + + private static string JoinMapChart(List joins) + { + if (joins.Count == 0) return ""; + var sb = new StringBuilder("### Join Maps\n\n"); + foreach (var kind in new[] { "Digital", "Analog", "Serial" }) + { + // Unrecognized join types (e.g. DigitalSerial) are listed as Digital + var ofKind = joins.Where(j => j.Type == kind || (kind == "Digital" && j.Type != "Analog" && j.Type != "Serial")).ToList(); + if (ofKind.Count == 0) continue; + sb.Append("#### ").Append(kind).Append("s\n\n"); + sb.Append("| Join | Type (RW) | Description |\n"); + sb.Append("| --- | --- | --- |\n"); + foreach (var j in ofKind) + sb.Append("| ").Append(j.Number).Append(" | ").Append(j.Access).Append(" | ").Append(j.Description ?? "None").Append(" |\n"); + sb.Append('\n'); + } + return sb.ToString(); + } + + // ---------------------------------------------------------------- config example + + private static readonly Regex ClassPattern = new Regex( + @"^\s*(?:\[[^\]]+\]\s*)*(?:public\s+|private\s+|protected\s+)?(?:partial\s+)?class\s+([A-Za-z_]\w*)(?:\s*:\s*[^\{]+)?\s*\{", + RegexOptions.Multiline); + + private static readonly Regex PropertyPattern = new Regex( + @"^\s*(?:\[[^\]]*\]\s*)*(?:public|private|protected)\s+(?:static\s+|virtual\s+|override\s+|abstract\s+|readonly\s+)?" + + @"([A-Za-z0-9_<>,\s\[\]\?]+?)\s+([A-Za-z_]\w*)\s*\{[^}]*?\}", + RegexOptions.Multiline | RegexOptions.Singleline); + + private static readonly Regex JsonPropertyPattern = new Regex(@"\[JsonProperty\(""([^""]+)""\)\]"); + + private class PropertyDef + { + public string JsonName; + public string Type; + /// Value from the property's <example> block, or null to generate one from Type. + public object Example; + } + + private class ClassDefs + { + public readonly List Keys = new List(); + private readonly Dictionary> _map = new Dictionary>(); + + public List this[string name] + { + get { return _map[name]; } + set + { + if (!_map.ContainsKey(name)) Keys.Add(name); + _map[name] = value; + } + } + + public bool Contains(string name) { return _map.ContainsKey(name); } + } + + private static ClassDefs ParseAllClasses(IList sources) + { + var defs = new ClassDefs(); + foreach (var source in sources) + { + var content = source.Content; + foreach (Match classMatch in ClassPattern.Matches(content)) + { + var body = ClassBody(content, classMatch.Index + classMatch.Length); + var properties = new List(); + foreach (Match prop in PropertyPattern.Matches(body)) + { + var json = JsonPropertyPattern.Match(prop.Value); + var jsonName = json.Success ? json.Groups[1].Value : prop.Groups[2].Value; + properties.Add(new PropertyDef + { + JsonName = jsonName, + Type = prop.Groups[1].Value.Trim(), + Example = ExampleValue(DocComment(body, prop.Index), jsonName), + }); + } + defs[classMatch.Groups[1].Value] = properties; + } + } + return defs; + } + + private static readonly Regex ExampleCodePattern = new Regex(@"(?:(?!).)*?(.*?)", RegexOptions.Singleline); + + // The /// lines directly above a member, without the slashes + private static string DocComment(string body, int memberIndex) + { + var lines = body.Substring(0, memberIndex).Split('\n'); + var doc = new List(); + for (var i = lines.Length - 1; i >= 0; i--) + { + var line = lines[i].Trim(); + if (line.StartsWith("///", StringComparison.Ordinal)) doc.Insert(0, line.Substring(3)); + else if (line.Length > 0 || doc.Count > 0) break; + } + return string.Join("\n", doc); + } + + // The value of jsonName in the first block that contains it, e.g. + // "control": { ... } or "properties": { "pollTimeMs": 30000 } + private static object ExampleValue(string doc, string jsonName) + { + foreach (Match m in ExampleCodePattern.Matches(doc)) + { + object parsed; + if (!Json.TryParse("{" + m.Groups[1].Value + "}", out parsed)) continue; + var queue = new Queue(); + queue.Enqueue(parsed); + while (queue.Count > 0) + { + var obj = queue.Dequeue() as Json.Object; + if (obj == null) continue; + foreach (var item in obj.Items) + { + if (item.Key == jsonName) return item.Value; + queue.Enqueue(item.Value); + } + } + } + return null; + } + + private static string ClassBody(string content, int start) + { + var depth = 1; + var index = start; + while (depth > 0 && index < content.Length) + { + if (content[index] == '{') depth++; + else if (content[index] == '}') depth--; + index++; + } + return content.Substring(start, Math.Max(0, index - 1 - start)); + } + + private static Json.Object SampleConfig(string configClass, ClassDefs defs, List supportedTypes) + { + var typeName = configClass.Substring(0, Math.Max(0, configClass.Length - 6)); + if (!supportedTypes.Contains(typeName) && supportedTypes.Count > 0) typeName = supportedTypes[0]; + var config = new Json.Object(); + config.Set("key", "device-1"); + config.Set("uid", 1); + config.Set("name", "Example Device"); + config.Set("type", typeName); + config.Set("group", "pluginDevices"); + config.Set("properties", SampleValue(configClass, defs, new HashSet())); + return config; + } + + // Values used when a property with one of these JSON names has no value of its own + private static readonly Dictionary DefaultValues = new Dictionary + { + { "pollTimeMs", new Json.Number("30000") }, + { "warningTimeoutMs", new Json.Number("180000") }, + { "errorTimeoutMs", new Json.Number("300000") }, + }; + + private static readonly string[] CollectionPrefixes = { "List<", "IList<", "IEnumerable<", "ObservableCollection<" }; + + private static object SampleValue(string propertyType, ClassDefs defs, HashSet processing) + { + var type = propertyType.Trim().TrimEnd('?'); + switch (type) + { + case "int": case "long": case "float": case "double": case "decimal": return 0; + case "string": return "SampleString"; + case "bool": return true; + case "DateTime": return "2021-01-01T00:00:00Z"; + } + if (CollectionPrefixes.Any(p => type.StartsWith(p, StringComparison.Ordinal))) + return new List { SampleValue(GenericArguments(type), defs, processing) }; + if (type.StartsWith("Dictionary<", StringComparison.Ordinal)) + { + var args = GenericArguments(type).Split(','); + var key = SampleValue(args[0].Trim(), defs, processing); + var value = args.Length > 1 ? SampleValue(args[1].Trim(), defs, processing) : "SampleValue"; + var dict = new Json.Object(); + dict.Set(Json.KeyString(key), value); + return dict; + } + if (defs.Contains(type)) + { + if (processing.Contains(type)) return new Json.Object(); + processing.Add(type); + var obj = new Json.Object(); + foreach (var prop in defs[type]) + { + object value; + if (prop.Example == null && DefaultValues.TryGetValue(prop.JsonName, out value)) { obj.Set(prop.JsonName, value); continue; } + obj.Set(prop.JsonName, prop.Example ?? SampleValue(prop.Type, defs, processing)); + } + processing.Remove(type); + return obj; + } + return "SampleValue"; + } + + private static string GenericArguments(string type) + { + var start = type.IndexOf('<') + 1; + return type.Substring(start, Math.Max(0, type.Length - 1 - start)); + } + + // ---------------------------------------------------------------- README sections + + private static string UpdateSection(string readme, string title, string content, bool honorSkip = true) + { + var start = ""; + var end = ""; + var match = Regex.Match(readme, Regex.Escape(start) + "(.*?)" + Regex.Escape(end), RegexOptions.Singleline | RegexOptions.IgnoreCase); + if (match.Success) + { + if (honorSkip && match.Groups[1].Value.Contains("")) return readme; + return readme.Substring(0, match.Index) + start + "\n" + content.TrimEnd() + "\n" + end + readme.Substring(match.Index + match.Length); + } + if (!readme.EndsWith("\n", StringComparison.Ordinal)) readme += "\n"; + return readme + start + "\n" + content.TrimEnd() + "\n" + end + "\n"; + } + + // Swaps the two marker blocks (with their contents) when Interfaces comes first + private static string BaseClassesBeforeInterfaces(string readme) + { + var interfaces = Regex.Match(readme, @"(?s).*?"); + var baseClasses = Regex.Match(readme, @"(?s).*?"); + if (!interfaces.Success || !baseClasses.Success || baseClasses.Index < interfaces.Index) return readme; + var between = interfaces.Index + interfaces.Length; + return readme.Substring(0, interfaces.Index) + baseClasses.Value + + readme.Substring(between, baseClasses.Index - between) + + interfaces.Value + readme.Substring(baseClasses.Index + baseClasses.Length); + } + + private static string SetSectionBody(string text, string name, string body) + { + var m = Regex.Match(text, "(?s)()(.*?)()"); + if (!m.Success || m.Groups[2].Value.Contains("")) return text; + return text.Substring(0, m.Index) + m.Groups[1].Value + body + m.Groups[3].Value + text.Substring(m.Index + m.Length); + } + + // ---------------------------------------------------------------- template post-processing + + // Config Example "type" = first TypeNames entry of the first file (by name) that sets TypeNames, + // so factories can have any class or file name (e.g. SonyBraviaDeviceFactory) + private static string FindConfigType(List byName) + { + var typeNames = new Regex(@"TypeNames\s*=\s*new\s+List\s*\(\s*\)\s*\{\s*""([^""]+)"""); + foreach (var source in byName) + { + var m = typeNames.Match(LineComment.Replace(source.Content, "")); + if (m.Success) return m.Groups[1].Value; + } + return null; + } + + // The unused "uid" is removed and "type" follows the factories + private static string FixConfigExample(string readme, string configType) + { + var section = Regex.Match(readme, @"(?s).*?"); + if (!section.Success) return readme; + var fixedText = Regex.Replace(section.Value, @"(?m)^[ \t]*""uid"":\s*\d+,?[ \t]*\n", ""); + if (configType != null) + { + var replacement = "\"type\": \"" + configType + "\""; + fixedText = new Regex(@"""type"":\s*""[^""]*""").Replace(fixedText, _ => replacement, 1); + } + return readme.Substring(0, section.Index) + fixedText + readme.Substring(section.Index + section.Length); + } + + // Base classes and interfaces declared by the plugin's own classes (not factories or join maps) + private static void CollectDeclaredTypes(List byName, List baseTypes, List interfaceTypes) + { + var classPattern = new Regex(@"(?m)^[ \t]*(?:(?:public|internal|abstract|sealed|static|partial)[ \t]+)*class[ \t]+(\w+)[ \t\r\n]*:([^{]+)\{"); + foreach (var source in byName) + { + var code = LineComment.Replace(BlockComment.Replace(source.Content, ""), ""); + foreach (Match m in classPattern.Matches(code)) + { + var bases = SplitTopLevel(Regex.Split(m.Groups[2].Value, @"\swhere\s")[0]); + if (bases.Contains("JoinMapBaseAdvanced") || IsFactory(m.Groups[1].Value, bases)) continue; + foreach (var b in bases) + { + var target = Regex.IsMatch(b, "^I[A-Z]") ? interfaceTypes : baseTypes; + if (!target.Contains(b)) target.Add(b); + } + } + } + } + + // Factories are recognized by their Essentials base class, whatever they are named + private static bool IsFactory(string className, List bases) + { + return className.EndsWith("Factory", StringComparison.Ordinal) + || bases.Any(b => Regex.IsMatch(b, @"^Essentials\w*Factory\s*<")); + } + + private static List SplitTopLevel(string list) + { + var parts = new List(); + var depth = 0; + var current = new StringBuilder(); + foreach (var ch in list) + { + if (ch == '<') depth++; + else if (ch == '>') depth--; + if (ch == ',' && depth == 0) { parts.Add(current.ToString().Trim()); current.Clear(); } + else current.Append(ch); + } + if (current.ToString().Trim().Length > 0) parts.Add(current.ToString().Trim()); + return parts; + } + + private static string DeclaredTypesBody(string title, List items) + { + if (items.Count == 0) return "\n"; + return "\n### " + title + "\n\n" + string.Join("\n", items.Select(i => "- " + i)) + "\n"; + } +} + +/// Minimal writer matching Python's json.dumps(value, indent=4). +public static class Json +{ + public class Object + { + public readonly List> Items = new List>(); + + // Python dict assignment: an existing key keeps its position + public void Set(string key, object value) + { + var i = Items.FindIndex(p => p.Key == key); + if (i >= 0) Items[i] = new KeyValuePair(key, value); + else Items.Add(new KeyValuePair(key, value)); + } + } + + public static string KeyString(object key) + { + if (key is bool) return (bool)key ? "true" : "false"; + if (key is int) return ((int)key).ToString(System.Globalization.CultureInfo.InvariantCulture); + return key as string ?? "SampleValue"; + } + + public static string Write(object value, int level) + { + var obj = value as Object; + if (obj != null) + { + if (obj.Items.Count == 0) return "{}"; + var inner = new string(' ', (level + 1) * 4); + return "{\n" + string.Join(",\n", obj.Items.Select(p => inner + Quote(p.Key) + ": " + Write(p.Value, level + 1))) + + "\n" + new string(' ', level * 4) + "}"; + } + var list = value as List; + if (list != null) + { + if (list.Count == 0) return "[]"; + var inner = new string(' ', (level + 1) * 4); + return "[\n" + string.Join(",\n", list.Select(v => inner + Write(v, level + 1))) + + "\n" + new string(' ', level * 4) + "]"; + } + if (value is bool) return (bool)value ? "true" : "false"; + if (value is int) return ((int)value).ToString(System.Globalization.CultureInfo.InvariantCulture); + if (value is Number) return ((Number)value).Text; + if (value == null) return "null"; + return Quote((string)value); + } + + /// A number parsed from an example, written back exactly as it was written. + public class Number + { + public Number(string text) { Text = text; } + public string Text { get; private set; } + } + + /// Lenient JSON reader for <example> blocks: allows trailing commas. + public static bool TryParse(string text, out object value) + { + var index = 0; + try + { + value = ParseValue(text, ref index); + SkipWhitespace(text, ref index); + return index == text.Length; + } + catch (FormatException) + { + value = null; + return false; + } + } + + private static object ParseValue(string text, ref int index) + { + SkipWhitespace(text, ref index); + if (index >= text.Length) throw new FormatException(); + var c = text[index]; + if (c == '{') + { + index++; + var obj = new Object(); + while (true) + { + SkipWhitespace(text, ref index); + if (index < text.Length && text[index] == '}') { index++; return obj; } + var key = ParseString(text, ref index); + SkipWhitespace(text, ref index); + Expect(text, ref index, ':'); + obj.Set(key, ParseValue(text, ref index)); + if (!NextItem(text, ref index, '}')) return obj; + } + } + if (c == '[') + { + index++; + var list = new List(); + while (true) + { + SkipWhitespace(text, ref index); + if (index < text.Length && text[index] == ']') { index++; return list; } + list.Add(ParseValue(text, ref index)); + if (!NextItem(text, ref index, ']')) return list; + } + } + if (c == '"') return ParseString(text, ref index); + var literal = Regex.Match(text.Substring(index), @"^(?:true|false|null|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)"); + if (!literal.Success) throw new FormatException(); + index += literal.Length; + switch (literal.Value) + { + case "true": return true; + case "false": return false; + case "null": return null; + default: return new Number(literal.Value); + } + } + + // After an item: true when another follows, false when the container closed + private static bool NextItem(string text, ref int index, char close) + { + SkipWhitespace(text, ref index); + if (index < text.Length && text[index] == ',') { index++; return true; } + Expect(text, ref index, close); + return false; + } + + private static string ParseString(string text, ref int index) + { + Expect(text, ref index, '"'); + var sb = new StringBuilder(); + while (index < text.Length && text[index] != '"') + { + var c = text[index++]; + if (c != '\\') { sb.Append(c); continue; } + if (index >= text.Length) throw new FormatException(); + var e = text[index++]; + switch (e) + { + case 'n': sb.Append('\n'); break; + case 'r': sb.Append('\r'); break; + case 't': sb.Append('\t'); break; + case 'b': sb.Append('\b'); break; + case 'f': sb.Append('\f'); break; + case 'u': + if (index + 4 > text.Length) throw new FormatException(); + sb.Append((char)Convert.ToInt32(text.Substring(index, 4), 16)); + index += 4; + break; + default: sb.Append(e); break; + } + } + Expect(text, ref index, '"'); + return sb.ToString(); + } + + private static void Expect(string text, ref int index, char c) + { + if (index >= text.Length || text[index] != c) throw new FormatException(); + index++; + } + + private static void SkipWhitespace(string text, ref int index) + { + while (index < text.Length && char.IsWhiteSpace(text[index])) index++; + } + + // ensure_ascii=True escaping + private static string Quote(string s) + { + var sb = new StringBuilder("\""); + foreach (var c in s) + { + switch (c) + { + case '"': sb.Append("\\\""); break; + case '\\': sb.Append("\\\\"); break; + case '\n': sb.Append("\\n"); break; + case '\r': sb.Append("\\r"); break; + case '\t': sb.Append("\\t"); break; + case '\b': sb.Append("\\b"); break; + case '\f': sb.Append("\\f"); break; + default: + if (c < 0x20 || c > 0x7E) sb.Append("\\u").Append(((int)c).ToString("x4")); + else sb.Append(c); + break; + } + } + return sb.Append('"').ToString(); + } +} diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 206b6c0..7b71b6a 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -4,11 +4,10 @@ $(Version) PepperDash Technology PepperDash Technology - PepperDash Essentials Plugin Template - Copyright © 2025 - https://github.com/PepperDash/EssentialsPluginTemplate.git + PepperDash Essentials Plugin Make Model + Copyright © 2026 + https://github.com/PepperDash/epi-make-model.git git - Crestron; 4series ..\output True LICENSE.md diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 95e93f1..f378f09 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -25,7 +25,8 @@ @@ -51,7 +52,7 @@ public class SyncReadmeBadges : Task [Required] public string ReadmePath { get; set; } [Required] public string TargetFramework { get; set; } public string EssentialsPackageVersion { get; set; } - public ITaskItem[] FactoryFiles { get; set; } + public ITaskItem[] SourceFiles { get; set; } public bool CheckOnly { get; set; } private static readonly Regex MinVersionPattern = @@ -63,7 +64,7 @@ public class SyncReadmeBadges : Task Version minVersion = null; string minText = null; - foreach (var item in FactoryFiles ?? new ITaskItem[0]) + foreach (var item in SourceFiles ?? new ITaskItem[0]) { var code = File.ReadAllText(item.GetMetadata("FullPath"), utf8); foreach (Match m in MinVersionPattern.Matches(code)) @@ -74,7 +75,7 @@ public class SyncReadmeBadges : Task } if (minVersion == null) { - Error("PDREADME001", "MinimumEssentialsFrameworkVersion not found in any *Factory.cs file"); + Error("PDREADME001", "no MinimumEssentialsFrameworkVersion = \"x.y.z\"; assignment found in the project's C# files"); return false; } @@ -173,10 +174,7 @@ public class SyncReadmeBadges : Task - - - <_ReadmeFactoryFiles Include="@(Compile)" Condition="$([System.String]::Copy('%(Filename)').EndsWith('Factory'))" /> - + <_ReadmeEssentialsVersion>@(PackageReference->WithMetadataValue('Identity', 'PepperDashEssentials')->'%(Version)') @@ -184,9 +182,9 @@ public class SyncReadmeBadges : Task - + @@ -197,6 +195,40 @@ public class SyncReadmeBadges : Task - + + + + + + <_ReadmeDocsEnabled Condition="'$(ProjectType)' == 'ProgramLibrary' And '$(DesignTimeBuild)' != 'true' And '$(TargetFramework)' != '' And Exists('$(ReadmePath)')">true + + + + + + + + + + + + <_ReadmeSource Include="@(Compile)" RelPath="$([MSBuild]::MakeRelative('$(MSBuildProjectDirectory)', '%(FullPath)'))" /> + <_ReadmeSource Remove="@(_ReadmeSource)" Condition="$([System.String]::Copy('%(RelPath)').StartsWith('..')) Or $([System.String]::Copy('%(RelPath)').StartsWith('obj')) Or $([System.String]::Copy('%(RelPath)').StartsWith('bin'))" /> + + + + + + + + + + diff --git a/src/MakeModelBridgeJoinMap.cs b/src/MakeModelBridgeJoinMap.cs index 72fee24..295a00a 100644 --- a/src/MakeModelBridgeJoinMap.cs +++ b/src/MakeModelBridgeJoinMap.cs @@ -1,6 +1,6 @@ using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Plugin +namespace PepperDash.Essentials.Plugins.MakeModel { /// /// Plugin device Bridge Join Map @@ -10,9 +10,9 @@ namespace PepperDash.Essentials.Plugin /// /// /// - /// "EssentialsPluginBridgeJoinMapTemplate" renamed to "SamsungMdcBridgeJoinMap" + /// "MakeModelBridgeJoinMap" renamed to "SamsungMdcBridgeJoinMap" /// - public class EssentialsPluginTemplateBridgeJoinMap : JoinMapBaseAdvanced + public class MakeModelBridgeJoinMap : JoinMapBaseAdvanced { #region Digital @@ -93,8 +93,8 @@ public class EssentialsPluginTemplateBridgeJoinMap : JoinMapBaseAdvanced /// Plugin device BridgeJoinMap constructor /// /// This will be the join it starts on the EISC bridge - public EssentialsPluginTemplateBridgeJoinMap(uint joinStart) - : base(joinStart, typeof(EssentialsPluginTemplateBridgeJoinMap)) + public MakeModelBridgeJoinMap(uint joinStart) + : base(joinStart, typeof(MakeModelBridgeJoinMap)) { } } diff --git a/src/MakeModelCrestronDevice.cs b/src/MakeModelCrestronDevice.cs index 6397168..c688b9f 100644 --- a/src/MakeModelCrestronDevice.cs +++ b/src/MakeModelCrestronDevice.cs @@ -7,7 +7,7 @@ using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Bridges; -namespace PepperDash.Essentials.Plugin +namespace PepperDash.Essentials.Plugins.MakeModel { /// /// Plugin device @@ -16,14 +16,14 @@ namespace PepperDash.Essentials.Plugin /// Rename the class to match the device plugin being developed. /// /// - /// "EssentialsPluginDeviceTemplate" renamed to "SamsungMdcDevice" + /// "MakeModelCrestronDevice" renamed to "CrestronTst1080Device" /// public class MakeModelCrestronDevice : CrestronGenericBridgeableBaseDevice { /// /// It is often desirable to store the config /// - private readonly MakeModelConfig config; + private readonly MakeModelPropertiesConfig config; #region Constructor for Devices without IBasicCommunication. Remove if not needed @@ -34,7 +34,7 @@ public class MakeModelCrestronDevice : CrestronGenericBridgeableBaseDevice /// /// /// - public MakeModelCrestronDevice(string key, string name, MakeModelConfig config, GenericBase hardware) + public MakeModelCrestronDevice(string key, string name, MakeModelPropertiesConfig config, GenericBase hardware) : base(key, name, hardware) { this.LogInformation("Constructing new {0} instance", name); @@ -60,7 +60,7 @@ public MakeModelCrestronDevice(string key, string name, MakeModelConfig config, /// public override void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge) { - var joinMap = new EssentialsPluginTemplateBridgeJoinMap(joinStart); + var joinMap = new MakeModelBridgeJoinMap(joinStart); // This adds the join map to the collection on the bridge bridge?.AddJoinMap(Key, joinMap); diff --git a/src/MakeModelCrestronDeviceFactory.cs b/src/MakeModelCrestronDeviceFactory.cs index e026892..7dd4476 100644 --- a/src/MakeModelCrestronDeviceFactory.cs +++ b/src/MakeModelCrestronDeviceFactory.cs @@ -3,7 +3,7 @@ using PepperDash.Core; using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Plugin +namespace PepperDash.Essentials.Plugins.MakeModel { /// @@ -13,7 +13,7 @@ namespace PepperDash.Essentials.Plugin /// Rename the class to match the device plugin being developed /// /// - /// "EssentialsPluginFactoryTemplate" renamed to "MyCrestronDeviceFactory" + /// "MakeModelCrestronDeviceFactory" renamed to "CrestronTst1080DeviceFactory" /// public class MakeModelCrestronDeviceFactory : EssentialsPluginDeviceFactory { @@ -26,11 +26,11 @@ public class MakeModelCrestronDeviceFactory : EssentialsPluginDeviceFactory /// Set the minimum Essentials Framework Version /// - /// MinimumEssentialsFrameworkVersion = "1.6.4; + /// MinimumEssentialsFrameworkVersion = "2.0.0"; /// /// In the constructor we initialize the list with the typenames that will build an instance of this device /// - /// TypeNames = new List() { "SamsungMdc", "SamsungMdcDisplay" }; + /// TypeNames = new List() { "crestronTst1080" }; /// /// public MakeModelCrestronDeviceFactory() @@ -45,7 +45,7 @@ public MakeModelCrestronDeviceFactory() } /// - /// Builds and returns an instance of EssentialsPluginTemplateCrestronDevice + /// Builds and returns an instance of MakeModelCrestronDevice /// /// device configuration /// plugin device or null @@ -60,7 +60,7 @@ public override EssentialsDevice BuildDevice(PepperDash.Essentials.Core.Config.D Debug.LogDebug("[{key}] Factory Attempting to create new device from type: {type}", dc.Key, dc.Type); // get the plugin device properties configuration object & check for null - var propertiesConfig = dc.Properties.ToObject(); + var propertiesConfig = dc.Properties.ToObject(); if (propertiesConfig == null) { Debug.LogWarning("[{key}] Factory: failed to read properties config for {name}", dc.Key, dc.Name); diff --git a/src/MakeModelDevice.cs b/src/MakeModelDevice.cs index 6a8c986..b6199e0 100644 --- a/src/MakeModelDevice.cs +++ b/src/MakeModelDevice.cs @@ -8,7 +8,7 @@ using PepperDash.Essentials.Core.Bridges; using PepperDash.Essentials.Core.Queues; -namespace PepperDash.Essentials.Plugin +namespace PepperDash.Essentials.Plugins.MakeModel { /// /// Plugin device template for third party devices that use IBasicCommunication @@ -17,14 +17,14 @@ namespace PepperDash.Essentials.Plugin /// Rename the class to match the device plugin being developed. /// /// - /// "EssentialsPluginDeviceTemplate" renamed to "SamsungMdcDevice" + /// "MakeModelDevice" renamed to "SamsungMdcDevice" /// public class MakeModelDevice : EssentialsBridgeableDevice { /// /// It is often desirable to store the config /// - private readonly MakeModelConfig config; + private readonly MakeModelPropertiesConfig config; /// /// Provides a queue and dedicated worker thread for processing feedback messages from a device. @@ -98,7 +98,7 @@ public bool Connect /// /// /// - public MakeModelDevice(string key, string name, MakeModelConfig config, IBasicCommunication comms) + public MakeModelDevice(string key, string name, MakeModelPropertiesConfig config, IBasicCommunication comms) : base(key, name) { this.LogInformation("Constructing new {0} instance", name); @@ -152,7 +152,7 @@ private void socket_ConnectionChange(object sender, GenericSocketStatusChageEven StatusFeedback?.FireUpdate(); } - // TODO [ ] If not using an API with a delimeter, delete the method below + // TODO [ ] If not using an API with a delimiter, delete the method below private void Handle_LineRecieved(object sender, GenericCommMethodReceiveTextArgs args) { // TODO [ ] Implement method @@ -161,14 +161,14 @@ private void Handle_LineRecieved(object sender, GenericCommMethodReceiveTextArgs receiveQueue.Enqueue(new ProcessStringMessage(args.Text, ProcessFeedbackMessage)); } - // TODO [ ] If not using an HEX/byte based API with no delimeter, delete the method below + // TODO [ ] If not using an HEX/byte based API with no delimiter, delete the method below private void Handle_BytesReceived(object sender, GenericCommMethodReceiveBytesArgs args) { // TODO [ ] Implement method throw new System.NotImplementedException(); } - // TODO [ ] If not using an ASCII based API with no delimeter, delete the method below + // TODO [ ] If not using an ASCII based API with no delimiter, delete the method below void Handle_TextReceived(object sender, GenericCommMethodReceiveTextArgs e) { // TODO [ ] Implement method @@ -184,8 +184,7 @@ void ProcessFeedbackMessage(string message) } - - // TODO [ ] If not using an ACII based API, delete the properties below + // TODO [ ] If not using an ASCII based API, delete the properties below /// /// Sends text to the device plugin comms /// @@ -242,7 +241,7 @@ public void Poll() /// public override void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge) { - var joinMap = new EssentialsPluginTemplateBridgeJoinMap(joinStart); + var joinMap = new MakeModelBridgeJoinMap(joinStart); // This adds the join map to the collection on the bridge bridge?.AddJoinMap(Key, joinMap); @@ -288,7 +287,6 @@ private void UpdateFeedbacks() } #endregion - } } diff --git a/src/MakeModelDeviceFactory.cs b/src/MakeModelDeviceFactory.cs index 4a05780..957025d 100644 --- a/src/MakeModelDeviceFactory.cs +++ b/src/MakeModelDeviceFactory.cs @@ -2,7 +2,7 @@ using PepperDash.Core; using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Plugin +namespace PepperDash.Essentials.Plugins.MakeModel { /// /// Plugin device factory for devices that use IBasicCommunication @@ -11,7 +11,7 @@ namespace PepperDash.Essentials.Plugin /// Rename the class to match the device plugin being developed /// /// - /// "EssentialsPluginFactoryTemplate" renamed to "MyDeviceFactory" + /// "MakeModelDeviceFactory" renamed to "SamsungMdcDeviceFactory" /// public class MakeModelDeviceFactory : EssentialsPluginDeviceFactory { @@ -24,7 +24,7 @@ public class MakeModelDeviceFactory : EssentialsPluginDeviceFactory /// Set the minimum Essentials Framework Version /// - /// MinimumEssentialsFrameworkVersion = "2.12.1; + /// MinimumEssentialsFrameworkVersion = "2.0.0"; /// /// In the constructor we initialize the list with the typenames that will build an instance of this device /// @@ -43,7 +43,7 @@ public MakeModelDeviceFactory() } /// - /// Builds and returns an instance of EssentialsPluginDeviceTemplate + /// Builds and returns an instance of MakeModelDevice /// /// device configuration /// plugin device or null @@ -57,7 +57,7 @@ public override EssentialsDevice BuildDevice(PepperDash.Essentials.Core.Config.D Debug.LogVerbose("[{key}] Factory Attempting to create new device from type: {type}", dc.Key, dc.Type); // get the plugin device properties configuration object & check for null - var propertiesConfig = dc.Properties.ToObject(); + var propertiesConfig = dc.Properties.ToObject(); if (propertiesConfig == null) { Debug.LogError("[{key}] Factory: failed to read properties config for {name}", dc.Key, dc.Name); @@ -65,7 +65,7 @@ public override EssentialsDevice BuildDevice(PepperDash.Essentials.Core.Config.D } // attempt build the plugin device comms device & check for null - // TODO { ] As of PepperDash Core 1.0.41, HTTP and HTTPS are not valid eControlMethods and will throw an exception. + // TODO [ ] As of PepperDash Core 1.0.41, HTTP and HTTPS are not valid eControlMethods and will throw an exception. var comms = CommFactory.CreateCommForDevice(dc); if (comms == null) { diff --git a/src/MakeModelLogicDevice.cs b/src/MakeModelLogicDevice.cs index 64b2f3b..5df3c60 100644 --- a/src/MakeModelLogicDevice.cs +++ b/src/MakeModelLogicDevice.cs @@ -4,7 +4,7 @@ using PepperDash.Essentials.Core; using PepperDash.Essentials.Core.Bridges; -namespace PepperDash.Essentials.Plugin +namespace PepperDash.Essentials.Plugins.MakeModel { /// /// Plugin device template for logic devices that don't communicate outside the program @@ -13,14 +13,14 @@ namespace PepperDash.Essentials.Plugin /// Rename the class to match the device plugin being developed. /// /// - /// "EssentialsPluginTemplateLogicDevice" renamed to "SamsungMdcDevice" + /// "MakeModelLogicDevice" renamed to "RoomSchedulerLogicDevice" /// public class MakeModelLogicDevice : EssentialsBridgeableDevice { /// /// It is often desirable to store the config /// - private readonly MakeModelConfig config; + private readonly MakeModelPropertiesConfig config; /// /// Plugin device constructor @@ -28,7 +28,7 @@ public class MakeModelLogicDevice : EssentialsBridgeableDevice /// /// /// - public MakeModelLogicDevice(string key, string name, MakeModelConfig config) + public MakeModelLogicDevice(string key, string name, MakeModelPropertiesConfig config) : base(key, name) { this.LogInformation("Constructing new {0} instance", name); @@ -49,7 +49,7 @@ public MakeModelLogicDevice(string key, string name, MakeModelConfig config) /// public override void LinkToApi(BasicTriList trilist, uint joinStart, string joinMapKey, EiscApiAdvanced bridge) { - var joinMap = new EssentialsPluginTemplateBridgeJoinMap(joinStart); + var joinMap = new MakeModelBridgeJoinMap(joinStart); // This adds the join map to the collection on the bridge bridge?.AddJoinMap(Key, joinMap); diff --git a/src/MakeModelLogicDeviceFactory.cs b/src/MakeModelLogicDeviceFactory.cs index fee4d47..f2db3c6 100644 --- a/src/MakeModelLogicDeviceFactory.cs +++ b/src/MakeModelLogicDeviceFactory.cs @@ -2,7 +2,7 @@ using PepperDash.Core; using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Plugin +namespace PepperDash.Essentials.Plugins.MakeModel { /// /// Plugin device factory for logic devices that don't communicate @@ -11,7 +11,7 @@ namespace PepperDash.Essentials.Plugin /// Rename the class to match the device plugin being developed /// /// - /// "EssentialsPluginFactoryTemplate" renamed to "MyLogicDeviceFactory" + /// "MakeModelLogicDeviceFactory" renamed to "RoomSchedulerLogicDeviceFactory" /// public class MakeModelLogicDeviceFactory : EssentialsPluginDeviceFactory { @@ -24,11 +24,11 @@ public class MakeModelLogicDeviceFactory : EssentialsPluginDeviceFactory /// Set the minimum Essentials Framework Version /// - /// MinimumEssentialsFrameworkVersion = "1.6.4; + /// MinimumEssentialsFrameworkVersion = "2.0.0"; /// /// In the constructor we initialize the list with the typenames that will build an instance of this device /// - /// TypeNames = new List() { "SamsungMdc", "SamsungMdcDisplay" }; + /// TypeNames = new List() { "roomScheduler" }; /// /// public MakeModelLogicDeviceFactory() @@ -43,7 +43,7 @@ public MakeModelLogicDeviceFactory() } /// - /// Builds and returns an instance of EssentialsPluginTemplateLogicDevice + /// Builds and returns an instance of MakeModelLogicDevice /// /// device configuration /// plugin device or null @@ -58,7 +58,7 @@ public override EssentialsDevice BuildDevice(PepperDash.Essentials.Core.Config.D Debug.LogDebug("[{key}] Factory Attempting to create new device from type: {type}", dc.Key, dc.Type); // get the plugin device properties configuration object & check for null - var propertiesConfig = dc.Properties.ToObject(); + var propertiesConfig = dc.Properties.ToObject(); if (propertiesConfig == null) { Debug.LogError("[{key}] Factory: failed to read properties config for {name}", dc.Key, dc.Name); diff --git a/src/MakeModelConfigObject.cs b/src/MakeModelPropertiesConfig.cs similarity index 86% rename from src/MakeModelConfigObject.cs rename to src/MakeModelPropertiesConfig.cs index 0492f38..4e30205 100644 --- a/src/MakeModelConfigObject.cs +++ b/src/MakeModelPropertiesConfig.cs @@ -2,7 +2,7 @@ using Newtonsoft.Json; using PepperDash.Essentials.Core; -namespace PepperDash.Essentials.Plugin +namespace PepperDash.Essentials.Plugins.MakeModel { /// /// Plugin device configuration object @@ -11,10 +11,10 @@ namespace PepperDash.Essentials.Plugin /// Rename the class to match the device plugin being created /// /// - /// "EssentialsPluginConfigObjectTemplate" renamed to "SamsungMdcConfig" + /// "MakeModelPropertiesConfig" renamed to "SamsungMdcPropertiesConfig" /// [ConfigSnippet("\"properties\":{\"control\":{}")] - public class MakeModelConfig + public class MakeModelPropertiesConfig { /// /// JSON control object @@ -25,9 +25,26 @@ public class MakeModelConfig /// In order to do so, you will need the username and password in the "tcpSshProperties" object. /// /// + /// TCP/IP (used for the README Config Example) /// /// "control": { /// "method": "tcpIp", + /// "tcpSshProperties": { + /// "address": "172.22.0.101", + /// "port": 23, + /// "username": "admin", + /// "password": "password", + /// "autoReconnect": true, + /// "autoReconnectIntervalMs": 10000 + /// } + /// } + /// + /// + /// + /// RS-232 on a processor COM port + /// + /// "control": { + /// "method": "com", /// "controlPortDevKey": "processor", /// "controlPortNumber": 1, /// "comParams": { @@ -38,14 +55,6 @@ public class MakeModelConfig /// "protocol": "RS232", /// "hardwareHandshake": "None", /// "softwareHandshake": "None" - /// }, - /// "tcpSshProperties": { - /// "address": "172.22.0.101", - /// "port": 23, - /// "username": "admin", - /// "password": "password", - /// "autoReconnect": true, - /// "autoReconnectIntervalMs": 10000 /// } /// } /// @@ -65,7 +74,7 @@ public class MakeModelConfig /// /// /// "properties": { - /// "polltimeMs": 30000 + /// "pollTimeMs": 30000 /// } /// /// @@ -119,27 +128,17 @@ public class MakeModelConfig /// /// /// "properties": { - /// "presets": { - /// "preset1": { - /// "enabled": true, - /// "name": "Preset 1" + /// "DeviceDictionary": { + /// "item1": { + /// "name": "Item 1 Name", + /// "value": 1 /// } /// } /// } /// /// - /// - /// - /// "properties": { - /// "inputNames": { - /// "input1": "Input 1", - /// "input2": "Input 2" - /// } - /// } - /// - /// [JsonProperty("DeviceDictionary")] - public Dictionary DeviceDictionary { get; set; } + public Dictionary DeviceDictionary { get; set; } /// /// Constuctor @@ -148,9 +147,9 @@ public class MakeModelConfig /// If using a collection you must instantiate the collection in the constructor /// to avoid exceptions when reading the configuration file /// - public MakeModelConfig() + public MakeModelPropertiesConfig() { - DeviceDictionary = new Dictionary(); + DeviceDictionary = new Dictionary(); } } @@ -163,16 +162,16 @@ public MakeModelConfig() /// /// /// "properties": { - /// "dictionary": { + /// "DeviceDictionary": { /// "item1": { /// "name": "Item 1 Name", - /// "value": "Item 1 Value" + /// "value": 1 /// } /// } /// } /// /// - public class MakeModelConfigDictionary + public class MakeModelPropertiesConfigDictionary { /// /// Serializes collection name property diff --git a/src/Properties/AssemblyInfo.cs b/src/Properties/AssemblyInfo.cs deleted file mode 100644 index 275e917..0000000 --- a/src/Properties/AssemblyInfo.cs +++ /dev/null @@ -1,8 +0,0 @@ -using System.Reflection; - -[assembly: AssemblyTitle("EssentialsPluginTemplateEpi")] -[assembly: AssemblyCompany("")] -[assembly: AssemblyProduct("EssentialsPluginTemplateEpi")] -[assembly: AssemblyCopyright("Copyright © 2022")] -[assembly: AssemblyVersion("1.0.0.*")] - diff --git a/src/Properties/ControlSystem.cfg b/src/Properties/ControlSystem.cfg deleted file mode 100644 index e69de29..0000000 diff --git a/src/epi-make-model.4Series.csproj b/src/epi-make-model.4Series.csproj index dae11e7..3753264 100644 --- a/src/epi-make-model.4Series.csproj +++ b/src/epi-make-model.4Series.csproj @@ -2,22 +2,18 @@ ProgramLibrary + net472 - PepperDash.Essentials.Plugin + PepperDash.Essentials.Plugins.MakeModel false - PepperDash.Essentials.Plugin.Make.Model - PepperDash Technology - This software is a template for a PepperDash Essentials Plugin. - Copyright 2026 - 1.0.0-local + PepperDash.Essentials.Plugins.Make.Model + This software is a PepperDash Essentials Plugin for a Make Model device. true - $(Version) bin\$(Configuration)\ - PepperDash Technology - Pepperdash.Essentials.Plugins.Template - https://github.com/PepperDash/EssentialsPluginTemplate.git - crestron 4series essentials plugin template + PepperDash.Essentials.Plugins.Make.Model + https://github.com/PepperDash/epi-make-model + crestron 4series essentials plugin @@ -25,26 +21,12 @@ - - - + - - - - - - runtime - - - - - - - - - - + + runtime + +