├── .gitattributes ├── .gitignore ├── CoAPExplorer.sln ├── Notes.md ├── Readme.md ├── Tests └── CoAPExplorer.WPF.Tests │ ├── CoAPExplorer.WPF.Tests.csproj │ ├── ConverterTests.cs │ ├── Properties │ └── AssemblyInfo.cs │ └── packages.config ├── Tools └── Database │ ├── CoapExplorerContext.cs │ ├── Database.csproj │ ├── Migrations │ ├── 20180516004057_Initial.Designer.cs │ ├── 20180516004057_Initial.cs │ └── CoapExplorerContextModelSnapshot.cs │ └── Program.cs ├── appveyor.yml ├── nuget.config └── src ├── CoAPExplorer.WPF ├── App.config ├── App.xaml ├── App.xaml.cs ├── CoAPExplorer.WPF.csproj ├── Consts.cs ├── Controls │ ├── AppBar.cs │ ├── Behaviors │ │ └── TextFieldBehavior.cs │ ├── CoapOptionsList.xaml │ ├── CoapOptionsList.xaml.cs │ ├── DeviceListView.cs │ ├── FilterOption.xaml │ ├── FilterOption.xaml.cs │ ├── NavigationDrawer.cs │ └── NavigationItem.cs ├── Converters │ ├── BoolToVisibilityConverter.cs │ ├── CoapExplorerIconConverter.cs │ ├── CoapMessageCodeToStringConverter.cs │ ├── CoapOptionTypeToNameConverter.cs │ ├── HextoAsciiConverter.cs │ ├── InvertBooleanConverter.cs │ ├── RelativeDateTimeConverter.cs │ └── UIElementVisibilityToUnsetValueConverter.cs ├── Dialogs │ ├── NewDeviceViewDialog.xaml │ └── NewDeviceViewDialog.xaml.cs ├── Extensions │ └── DependencyObjectExtensions.cs ├── MainWindow.xaml ├── MainWindow.xaml.cs ├── MockViewModels │ ├── DeviceListMock.cs │ ├── DeviceNavigationViewModel.cs │ ├── DeviceViewModel.cs │ ├── MockCoapOptionsList.cs │ ├── MockHomeView.cs │ └── NavigationViewModel.cs ├── Properties │ ├── AssemblyInfo.cs │ ├── Resources.Designer.cs │ ├── Resources.resx │ ├── Settings.Designer.cs │ └── Settings.settings ├── Resources │ ├── JSONFormat.xml │ ├── LinkFormat.xml │ └── logo.ico ├── Services │ ├── AvalonEditTextMarkerService.cs │ └── CoapFormatHighlightManager.cs ├── Themes │ ├── AppBar.xaml │ ├── DeviceListView.xaml │ └── NavigationDrawer.xaml └── Views │ ├── DeviceNavigationView.xaml │ ├── DeviceNavigationView.xaml.cs │ ├── DeviceView.xaml │ ├── DeviceView.xaml.cs │ ├── HomeView.xaml │ ├── HomeView.xaml.cs │ ├── MessageRequestView.xaml │ ├── MessageRequestView.xaml.cs │ ├── MessageResponseView.xaml │ ├── MessageResponseView.xaml.cs │ ├── NavigationView.xaml │ ├── NavigationView.xaml.cs │ ├── RecentDevicesView.xaml │ ├── RecentDevicesView.xaml.cs │ ├── SearchView.xaml │ └── SearchView.xaml.cs └── CoAPExplorer ├── App.cs ├── CoAPExplorer.csproj ├── CoapIcon.cs ├── Database ├── CoapExplorerContext.cs ├── CoapOptionSerialiser.cs └── Migrations │ ├── 20180516004057_Initial.Designer.cs │ ├── 20180516004057_Initial.cs │ └── CoapExplorerContextModelSnapshot.cs ├── EndpointType.cs ├── Extensions ├── CoapMessageExtensions.cs ├── EnumExtensions.cs └── ReactiveLoggerExtensions.cs ├── FormattedTextException.cs ├── Models ├── Device.cs ├── DeviceResource.cs ├── Message.cs ├── NavigationItem.cs ├── RequestFilter.cs └── ToastNotification.cs ├── Properties └── AssemblyInfo.cs ├── Services ├── CoapEndpointFactory.cs ├── CoapService.cs ├── DiscoveryService.cs ├── IDiscoveryService.cs └── MyDebugLogger.cs ├── Utils ├── CoapPayloadFormatConverter.cs └── StringEscape.cs └── ViewModels ├── DeviceNavigationViewModel.cs ├── DeviceViewModel.cs ├── HomeViewModel.cs ├── MessageViewModel.cs ├── NavigationViewModel.cs ├── NewDeviceViewModel.cs ├── RecentDevicesViewModel.cs └── SearchViewModel.cs /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.userosscache 8 | *.sln.docstates 9 | 10 | # User-specific files (MonoDevelop/Xamarin Studio) 11 | *.userprefs 12 | 13 | # Build results 14 | [Dd]ebug/ 15 | [Dd]ebugPublic/ 16 | [Rr]elease/ 17 | [Rr]eleases/ 18 | x64/ 19 | x86/ 20 | bld/ 21 | [Bb]in/ 22 | [Oo]bj/ 23 | [Ll]og/ 24 | 25 | # Visual Studio 2015 cache/options directory 26 | .vs/ 27 | # Uncomment if you have tasks that create the project's static files in wwwroot 28 | #wwwroot/ 29 | 30 | # MSTest test Results 31 | [Tt]est[Rr]esult*/ 32 | [Bb]uild[Ll]og.* 33 | 34 | # NUNIT 35 | *.VisualState.xml 36 | TestResult.xml 37 | 38 | # Build Results of an ATL Project 39 | [Dd]ebugPS/ 40 | [Rr]eleasePS/ 41 | dlldata.c 42 | 43 | # DNX 44 | project.lock.json 45 | project.fragment.lock.json 46 | artifacts/ 47 | 48 | *_i.c 49 | *_p.c 50 | *_i.h 51 | *.ilk 52 | *.meta 53 | *.obj 54 | *.pch 55 | *.pdb 56 | *.pgc 57 | *.pgd 58 | *.rsp 59 | *.sbr 60 | *.tlb 61 | *.tli 62 | *.tlh 63 | *.tmp 64 | *.tmp_proj 65 | *.log 66 | *.vspscc 67 | *.vssscc 68 | .builds 69 | *.pidb 70 | *.svclog 71 | *.scc 72 | 73 | # Chutzpah Test files 74 | _Chutzpah* 75 | 76 | # Visual C++ cache files 77 | ipch/ 78 | *.aps 79 | *.ncb 80 | *.opendb 81 | *.opensdf 82 | *.sdf 83 | *.cachefile 84 | *.VC.db 85 | *.VC.VC.opendb 86 | 87 | # Visual Studio profiler 88 | *.psess 89 | *.vsp 90 | *.vspx 91 | *.sap 92 | 93 | # TFS 2012 Local Workspace 94 | $tf/ 95 | 96 | # Guidance Automation Toolkit 97 | *.gpState 98 | 99 | # ReSharper is a .NET coding add-in 100 | _ReSharper*/ 101 | *.[Rr]e[Ss]harper 102 | *.DotSettings.user 103 | 104 | # JustCode is a .NET coding add-in 105 | .JustCode 106 | 107 | # TeamCity is a build add-in 108 | _TeamCity* 109 | 110 | # DotCover is a Code Coverage Tool 111 | *.dotCover 112 | 113 | # NCrunch 114 | _NCrunch_* 115 | .*crunch*.local.xml 116 | nCrunchTemp_* 117 | 118 | # MightyMoose 119 | *.mm.* 120 | AutoTest.Net/ 121 | 122 | # Web workbench (sass) 123 | .sass-cache/ 124 | 125 | # Installshield output folder 126 | [Ee]xpress/ 127 | 128 | # DocProject is a documentation generator add-in 129 | DocProject/buildhelp/ 130 | DocProject/Help/*.HxT 131 | DocProject/Help/*.HxC 132 | DocProject/Help/*.hhc 133 | DocProject/Help/*.hhk 134 | DocProject/Help/*.hhp 135 | DocProject/Help/Html2 136 | DocProject/Help/html 137 | 138 | # Click-Once directory 139 | publish/ 140 | 141 | # Publish Web Output 142 | *.[Pp]ublish.xml 143 | *.azurePubxml 144 | # TODO: Comment the next line if you want to checkin your web deploy settings 145 | # but database connection strings (with potential passwords) will be unencrypted 146 | #*.pubxml 147 | *.publishproj 148 | 149 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 150 | # checkin your Azure Web App publish settings, but sensitive information contained 151 | # in these scripts will be unencrypted 152 | PublishScripts/ 153 | 154 | # NuGet Packages 155 | *.nupkg 156 | # The packages folder can be ignored because of Package Restore 157 | **/packages/* 158 | # except build/, which is used as an MSBuild target. 159 | !**/packages/build/ 160 | # Uncomment if necessary however generally it will be regenerated when needed 161 | #!**/packages/repositories.config 162 | # NuGet v3's project.json files produces more ignoreable files 163 | *.nuget.props 164 | *.nuget.targets 165 | 166 | # Microsoft Azure Build Output 167 | csx/ 168 | *.build.csdef 169 | 170 | # Microsoft Azure Emulator 171 | ecf/ 172 | rcf/ 173 | 174 | # Windows Store app package directories and files 175 | AppPackages/ 176 | BundleArtifacts/ 177 | Package.StoreAssociation.xml 178 | _pkginfo.txt 179 | 180 | # Visual Studio cache files 181 | # files ending in .cache can be ignored 182 | *.[Cc]ache 183 | # but keep track of directories ending in .cache 184 | !*.[Cc]ache/ 185 | 186 | # Others 187 | ClientBin/ 188 | ~$* 189 | *~ 190 | *.dbmdl 191 | *.dbproj.schemaview 192 | *.jfm 193 | *.pfx 194 | *.publishsettings 195 | node_modules/ 196 | orleans.codegen.cs 197 | *.[Dd]esigner.cs 198 | 199 | # Since there are multiple workflows, uncomment next line to ignore bower_components 200 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 201 | #bower_components/ 202 | 203 | # RIA/Silverlight projects 204 | Generated_Code/ 205 | 206 | # Backup & report files from converting an old project file 207 | # to a newer Visual Studio version. Backup files are not needed, 208 | # because we have git ;-) 209 | _UpgradeReport_Files/ 210 | Backup*/ 211 | UpgradeLog*.XML 212 | UpgradeLog*.htm 213 | 214 | # SQL Server files 215 | *.mdf 216 | *.ldf 217 | 218 | # Business Intelligence projects 219 | *.rdl.data 220 | *.bim.layout 221 | *.bim_*.settings 222 | 223 | # Microsoft Fakes 224 | FakesAssemblies/ 225 | 226 | # GhostDoc plugin setting file 227 | *.GhostDoc.xml 228 | 229 | # Node.js Tools for Visual Studio 230 | .ntvs_analysis.dat 231 | 232 | # Visual Studio 6 build log 233 | *.plg 234 | 235 | # Visual Studio 6 workspace options file 236 | *.opt 237 | 238 | # Visual Studio LightSwitch build output 239 | **/*.HTMLClient/GeneratedArtifacts 240 | **/*.DesktopClient/GeneratedArtifacts 241 | **/*.DesktopClient/ModelManifest.xml 242 | **/*.Server/GeneratedArtifacts 243 | **/*.Server/ModelManifest.xml 244 | _Pvt_Extensions 245 | 246 | # Paket dependency manager 247 | .paket/paket.exe 248 | paket-files/ 249 | 250 | # FAKE - F# Make 251 | .fake/ 252 | 253 | # JetBrains Rider 254 | .idea/ 255 | *.sln.iml 256 | 257 | # CodeRush 258 | .cr/ 259 | 260 | # Python Tools for Visual Studio (PTVS) 261 | __pycache__/ 262 | *.pyc -------------------------------------------------------------------------------- /CoAPExplorer.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27130.2036 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoAPExplorer.WPF", "src\CoAPExplorer.WPF\CoAPExplorer.WPF.csproj", "{9BEC4D6B-5BF2-4893-9E1E-17882B13D5E6}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{02E79D79-5DE2-4FD2-921B-643147B0FDA7}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CoAPExplorer", "src\CoAPExplorer\CoAPExplorer.csproj", "{A18BDC5C-947F-4ACB-9413-F85E78D2704E}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{F2D4903E-1237-405F-B41A-C94931BAECEE}" 13 | ProjectSection(SolutionItems) = preProject 14 | .gitignore = .gitignore 15 | appveyor.yml = appveyor.yml 16 | Notes.md = Notes.md 17 | nuget.config = nuget.config 18 | Readme.md = Readme.md 19 | EndProjectSection 20 | EndProject 21 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tools", "Tools", "{6E6E4765-D2A5-43F8-BD2C-669AF6DCC78D}" 22 | EndProject 23 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Database", "Tools\Database\Database.csproj", "{B1BD0B92-5936-4E98-B13E-CD5D5D239A04}" 24 | EndProject 25 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{9C82C172-B4A0-4C94-ADB3-ED7CB18C8439}" 26 | EndProject 27 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CoAPExplorer.WPF.Tests", "Tests\CoAPExplorer.WPF.Tests\CoAPExplorer.WPF.Tests.csproj", "{A6D994B5-DC66-466D-A460-F5042EDA4D5F}" 28 | EndProject 29 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Media", "Media", "{C3297368-6E89-4E86-8386-79807CC31156}" 30 | EndProject 31 | Global 32 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 33 | Debug|Any CPU = Debug|Any CPU 34 | Release|Any CPU = Release|Any CPU 35 | EndGlobalSection 36 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 37 | {9BEC4D6B-5BF2-4893-9E1E-17882B13D5E6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {9BEC4D6B-5BF2-4893-9E1E-17882B13D5E6}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {9BEC4D6B-5BF2-4893-9E1E-17882B13D5E6}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {9BEC4D6B-5BF2-4893-9E1E-17882B13D5E6}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {A18BDC5C-947F-4ACB-9413-F85E78D2704E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {A18BDC5C-947F-4ACB-9413-F85E78D2704E}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {A18BDC5C-947F-4ACB-9413-F85E78D2704E}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {A18BDC5C-947F-4ACB-9413-F85E78D2704E}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {B1BD0B92-5936-4E98-B13E-CD5D5D239A04}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {B1BD0B92-5936-4E98-B13E-CD5D5D239A04}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {B1BD0B92-5936-4E98-B13E-CD5D5D239A04}.Release|Any CPU.ActiveCfg = Release|Any CPU 48 | {B1BD0B92-5936-4E98-B13E-CD5D5D239A04}.Release|Any CPU.Build.0 = Release|Any CPU 49 | {A6D994B5-DC66-466D-A460-F5042EDA4D5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {A6D994B5-DC66-466D-A460-F5042EDA4D5F}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {A6D994B5-DC66-466D-A460-F5042EDA4D5F}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {A6D994B5-DC66-466D-A460-F5042EDA4D5F}.Release|Any CPU.Build.0 = Release|Any CPU 53 | EndGlobalSection 54 | GlobalSection(SolutionProperties) = preSolution 55 | HideSolutionNode = FALSE 56 | EndGlobalSection 57 | GlobalSection(NestedProjects) = preSolution 58 | {9BEC4D6B-5BF2-4893-9E1E-17882B13D5E6} = {02E79D79-5DE2-4FD2-921B-643147B0FDA7} 59 | {A18BDC5C-947F-4ACB-9413-F85E78D2704E} = {02E79D79-5DE2-4FD2-921B-643147B0FDA7} 60 | {B1BD0B92-5936-4E98-B13E-CD5D5D239A04} = {6E6E4765-D2A5-43F8-BD2C-669AF6DCC78D} 61 | {A6D994B5-DC66-466D-A460-F5042EDA4D5F} = {9C82C172-B4A0-4C94-ADB3-ED7CB18C8439} 62 | {C3297368-6E89-4E86-8386-79807CC31156} = {F2D4903E-1237-405F-B41A-C94931BAECEE} 63 | EndGlobalSection 64 | GlobalSection(ExtensibilityGlobals) = postSolution 65 | SolutionGuid = {F1A27755-63B4-411F-A681-40D8C089EBA6} 66 | EndGlobalSection 67 | EndGlobal 68 | -------------------------------------------------------------------------------- /Notes.md: -------------------------------------------------------------------------------- 1 | ## Discovery 2 | - Networks 3 | - Broadcast (i.e. 255.255.255.255) 4 | - Multicast IPv4/IPv6 5 | - `/.well-known/core` 6 | - Filter parameters (rt=) 7 | - CoRE resource Directory 8 | - See draft-ietf-core-resource-directory 9 | - Save last search urls 10 | --- 11 | 12 | - Investigate User Input Validation 13 | 14 | Create a App class that will be inherited by the Xamarin.Forms App class. 15 | - sets up viewmodels and reactive views 16 | - other DI stuff 17 | 18 | Coap.Net 19 | - If your message has options set, but forgot to set the Code to anyhting other than None, there's no indication of failure or forgotten field. Maybe log it? or be explicit? -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # CoAP Explorer 2 | 3 | [![Build status](https://ci.appveyor.com/api/projects/status/njym61gix1mygnqg/branch/master?svg=true)](https://ci.appveyor.com/project/NZSmartie/coapexplorer/branch/master) 4 | 5 | Work in Progress App for interacting with CoAP devices. Soon to be cross platform, for now, is targeting Windows. 6 | 7 | Thanks To: 8 | - [ReactiveUI](https://github.com/reactiveui/ReactiveUI/) - Reactive Style UI 9 | - [ReactiveUI.Routing](https://github.com/KallynGowdy/ReactiveUI.Routing) - Better cross platform routing library for ReactiveUI 10 | - [Material Deisgn Toolkit](https://github.com/ButchersBoy/MaterialDesignInXamlToolkit) Google's Material Design for WIndows Presentation Framework 11 | - [AvalonEdit](https://github.com/icsharpcode/AvalonEdit) - Text Highlighter for WPF 12 | - [CoAP.Net](https://github.com/NZSmartie/CoAP.Net/) - My very own CoAP library 13 | 14 | Latest nightly builds for Windows can be downloaded straight from AppVeyor - https://ci.appveyor.com/project/NZSmartie/coapexplorer/build/artifacts 15 | 16 | ## Goals 17 | 18 | - Cross Platform 19 | - Using the same concepts from Xamarin Apps, the core functionality is in the schared project (Targeting .Net Standard) 20 | - Device Discovery 21 | - [X] UDP Multicast Discovery. 22 | - [ ] TODO: Suport more transports. 23 | - Fully functioal message editor 24 | - Support various content types 25 | - Saving message requests or responses 26 | - Easy UI 27 | 28 | ## Screen Grabs 29 | 30 | IMAGE ALT TEXT HERE -------------------------------------------------------------------------------- /Tests/CoAPExplorer.WPF.Tests/CoAPExplorer.WPF.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | Debug 7 | AnyCPU 8 | {A6D994B5-DC66-466D-A460-F5042EDA4D5F} 9 | Library 10 | Properties 11 | CoAPExplorer.WPF.Tests 12 | CoAPExplorer.WPF.Tests 13 | v4.7.1 14 | 512 15 | {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} 16 | 15.0 17 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) 18 | $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages 19 | False 20 | UnitTest 21 | 22 | 23 | 24 | 25 | 26 | true 27 | full 28 | false 29 | bin\Debug\ 30 | DEBUG;TRACE 31 | prompt 32 | 4 33 | latest 34 | 35 | 36 | pdbonly 37 | true 38 | bin\Release\ 39 | TRACE 40 | prompt 41 | 4 42 | latest 43 | 44 | 45 | 46 | ..\..\packages\NUnit.3.10.1\lib\net45\nunit.framework.dll 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | {9bec4d6b-5bf2-4893-9e1e-17882b13d5e6} 61 | CoAPExplorer.WPF 62 | 63 | 64 | 65 | 66 | 67 | 68 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}. 69 | 70 | 71 | 72 | 73 | -------------------------------------------------------------------------------- /Tests/CoAPExplorer.WPF.Tests/ConverterTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using CoAPExplorer.WPF.Converters; 4 | using NUnit.Framework; 5 | 6 | 7 | namespace CoAPExplorer.WPF.Tests 8 | { 9 | [TestFixture] 10 | public class ConverterTests 11 | { 12 | [TestCase(new byte[] { 0x12, 0x34 }, 2, ExpectedResult = "12 34")] 13 | [TestCase(new byte[] { 0x12, 0x34 }, 4, ExpectedResult = "12 34 ")] 14 | [TestCase(new byte[] { 0x1}, 4, ExpectedResult = "1")] 15 | [TestCase(new byte[] { 0x12 }, 4, ExpectedResult = "12 ")] 16 | [TestCase(new byte[] { }, 4, ExpectedResult = "")] 17 | [TestCase(new byte[] { 0x0 }, 1, ExpectedResult = "0")] 18 | [TestCase(new byte[] { 0x15, 0x1F }, 1, ExpectedResult = "15")] 19 | public string HexToAsciiConverter_Convert(byte[] data, int maxBytes) 20 | { 21 | var converter = new HextoAsciiConverter(); 22 | 23 | return converter.Convert(data, typeof(string), maxBytes, CultureInfo.CurrentCulture); 24 | } 25 | 26 | [TestCase("12 34", 2, ExpectedResult = new byte[] { 0x12, 0x34 })] 27 | [TestCase("12 34 ", 4, ExpectedResult = new byte[] { 0x12, 0x34 })] 28 | [TestCase("1", 4, ExpectedResult = new byte[] { 0x1 })] 29 | [TestCase("12 ", 4, ExpectedResult = new byte[] { 0x12 })] 30 | [TestCase("", 4, ExpectedResult = new byte[] { })] 31 | [TestCase("0", 1, ExpectedResult = new byte[] { 0x0 })] 32 | [TestCase("15 1F", 1, ExpectedResult = new byte[] { 0x15 })] 33 | public byte[] HexToAsciiConverter_ConvertBack(string data, int maxBytes) 34 | { 35 | var converter = new HextoAsciiConverter(); 36 | 37 | return converter.ConvertBack(data, typeof(string), maxBytes, CultureInfo.CurrentCulture); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /Tests/CoAPExplorer.WPF.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | [assembly: AssemblyTitle("CoAPExplorer.WPF.Tests")] 6 | [assembly: AssemblyDescription("")] 7 | [assembly: AssemblyConfiguration("")] 8 | [assembly: AssemblyCompany("")] 9 | [assembly: AssemblyProduct("CoAPExplorer.WPF.Tests")] 10 | [assembly: AssemblyCopyright("Copyright © 2018")] 11 | [assembly: AssemblyTrademark("")] 12 | [assembly: AssemblyCulture("")] 13 | 14 | [assembly: ComVisible(false)] 15 | 16 | [assembly: Guid("a6d994b5-dc66-466d-a460-f5042eda4d5f")] 17 | 18 | // [assembly: AssemblyVersion("1.0.*")] 19 | [assembly: AssemblyVersion("1.0.0.0")] 20 | [assembly: AssemblyFileVersion("1.0.0.0")] 21 | -------------------------------------------------------------------------------- /Tests/CoAPExplorer.WPF.Tests/packages.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /Tools/Database/CoapExplorerContext.cs: -------------------------------------------------------------------------------- 1 | using CoAPExplorer.Models; 2 | using CoAPExplorer.Services; 3 | using Microsoft.EntityFrameworkCore; 4 | using Microsoft.EntityFrameworkCore.Design; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Text; 8 | 9 | namespace CoAPExplorer.Database 10 | { 11 | public class CoapExplorerContextFactory : IDesignTimeDbContextFactory 12 | { 13 | public CoapExplorerContext CreateDbContext(string[] args) 14 | { 15 | var optionsBuilder = new DbContextOptionsBuilder(); 16 | optionsBuilder.UseSqlite("Filename=something.db", b => b.MigrationsAssembly("Database")); 17 | 18 | return new CoapExplorerContext(optionsBuilder.Options); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Tools/Database/Database.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Exe 5 | netcoreapp2.0 6 | CoAPExplorer.Database 7 | latest 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Tools/Database/Migrations/20180516004057_Initial.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using CoAPExplorer; 3 | using CoAPExplorer.Database; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using Microsoft.EntityFrameworkCore.Migrations; 8 | using Microsoft.EntityFrameworkCore.Storage; 9 | using Microsoft.EntityFrameworkCore.Storage.Internal; 10 | using System; 11 | 12 | namespace CoAPExplorer.Database.Migrations 13 | { 14 | [DbContext(typeof(CoapExplorerContext))] 15 | [Migration("20180516004057_Initial")] 16 | partial class Initial 17 | { 18 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 19 | { 20 | #pragma warning disable 612, 618 21 | modelBuilder 22 | .HasAnnotation("ProductVersion", "2.0.2-rtm-10011"); 23 | 24 | modelBuilder.Entity("CoAPExplorer.Models.Device", b => 25 | { 26 | b.Property("Id") 27 | .ValueGeneratedOnAdd(); 28 | 29 | b.Property("EndpointType"); 30 | 31 | b.Property("IsFavourite"); 32 | 33 | b.Property("LastSeen"); 34 | 35 | b.Property("Name"); 36 | 37 | b.Property("_dbAddress") 38 | .HasColumnName("Address"); 39 | 40 | b.HasKey("Id"); 41 | 42 | b.ToTable("Devices"); 43 | }); 44 | 45 | modelBuilder.Entity("CoAPExplorer.Models.DeviceResource", b => 46 | { 47 | b.Property("Id") 48 | .ValueGeneratedOnAdd(); 49 | 50 | b.Property("DeviceId"); 51 | 52 | b.Property("Name"); 53 | 54 | b.Property("_dbContentFormat") 55 | .HasColumnName("ContentFormat"); 56 | 57 | b.Property("_dbUrl") 58 | .HasColumnName("Url"); 59 | 60 | b.HasKey("Id"); 61 | 62 | b.HasIndex("DeviceId"); 63 | 64 | b.ToTable("DeviceResource"); 65 | }); 66 | 67 | modelBuilder.Entity("CoAPExplorer.Models.Message", b => 68 | { 69 | b.Property("Id") 70 | .ValueGeneratedOnAdd(); 71 | 72 | b.Property("Payload"); 73 | 74 | b.Property("_dbCode") 75 | .HasColumnName("Code"); 76 | 77 | b.Property("_dbContentFormat") 78 | .HasColumnName("ContentFormat"); 79 | 80 | b.Property("_dbOptions") 81 | .HasColumnName("Options"); 82 | 83 | b.Property("_dbUrl") 84 | .HasColumnName("Url"); 85 | 86 | b.HasKey("Id"); 87 | 88 | b.ToTable("RecentMessages"); 89 | }); 90 | 91 | modelBuilder.Entity("CoAPExplorer.Models.DeviceResource", b => 92 | { 93 | b.HasOne("CoAPExplorer.Models.Device", "Device") 94 | .WithMany("KnownResources") 95 | .HasForeignKey("DeviceId"); 96 | }); 97 | #pragma warning restore 612, 618 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Tools/Database/Migrations/20180516004057_Initial.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace CoAPExplorer.Database.Migrations 6 | { 7 | public partial class Initial : Migration 8 | { 9 | protected override void Up(MigrationBuilder migrationBuilder) 10 | { 11 | migrationBuilder.CreateTable( 12 | name: "Devices", 13 | columns: table => new 14 | { 15 | Id = table.Column(nullable: false) 16 | .Annotation("Sqlite:Autoincrement", true), 17 | EndpointType = table.Column(nullable: false), 18 | IsFavourite = table.Column(nullable: false), 19 | LastSeen = table.Column(nullable: false), 20 | Name = table.Column(nullable: true), 21 | Address = table.Column(nullable: true) 22 | }, 23 | constraints: table => 24 | { 25 | table.PrimaryKey("PK_Devices", x => x.Id); 26 | }); 27 | 28 | migrationBuilder.CreateTable( 29 | name: "RecentMessages", 30 | columns: table => new 31 | { 32 | Id = table.Column(nullable: false) 33 | .Annotation("Sqlite:Autoincrement", true), 34 | Payload = table.Column(nullable: true), 35 | Code = table.Column(nullable: true), 36 | ContentFormat = table.Column(nullable: true), 37 | Options = table.Column(nullable: true), 38 | Url = table.Column(nullable: true) 39 | }, 40 | constraints: table => 41 | { 42 | table.PrimaryKey("PK_RecentMessages", x => x.Id); 43 | }); 44 | 45 | migrationBuilder.CreateTable( 46 | name: "DeviceResource", 47 | columns: table => new 48 | { 49 | Id = table.Column(nullable: false) 50 | .Annotation("Sqlite:Autoincrement", true), 51 | DeviceId = table.Column(nullable: true), 52 | Name = table.Column(nullable: true), 53 | ContentFormat = table.Column(nullable: true), 54 | Url = table.Column(nullable: true) 55 | }, 56 | constraints: table => 57 | { 58 | table.PrimaryKey("PK_DeviceResource", x => x.Id); 59 | table.ForeignKey( 60 | name: "FK_DeviceResource_Devices_DeviceId", 61 | column: x => x.DeviceId, 62 | principalTable: "Devices", 63 | principalColumn: "Id", 64 | onDelete: ReferentialAction.Restrict); 65 | }); 66 | 67 | migrationBuilder.CreateIndex( 68 | name: "IX_DeviceResource_DeviceId", 69 | table: "DeviceResource", 70 | column: "DeviceId"); 71 | } 72 | 73 | protected override void Down(MigrationBuilder migrationBuilder) 74 | { 75 | migrationBuilder.DropTable( 76 | name: "DeviceResource"); 77 | 78 | migrationBuilder.DropTable( 79 | name: "RecentMessages"); 80 | 81 | migrationBuilder.DropTable( 82 | name: "Devices"); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /Tools/Database/Migrations/CoapExplorerContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using CoAPExplorer; 3 | using CoAPExplorer.Database; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using Microsoft.EntityFrameworkCore.Migrations; 8 | using Microsoft.EntityFrameworkCore.Storage; 9 | using Microsoft.EntityFrameworkCore.Storage.Internal; 10 | using System; 11 | 12 | namespace CoAPExplorer.Database.Migrations 13 | { 14 | [DbContext(typeof(CoapExplorerContext))] 15 | partial class CoapExplorerContextModelSnapshot : ModelSnapshot 16 | { 17 | protected override void BuildModel(ModelBuilder modelBuilder) 18 | { 19 | #pragma warning disable 612, 618 20 | modelBuilder 21 | .HasAnnotation("ProductVersion", "2.0.2-rtm-10011"); 22 | 23 | modelBuilder.Entity("CoAPExplorer.Models.Device", b => 24 | { 25 | b.Property("Id") 26 | .ValueGeneratedOnAdd(); 27 | 28 | b.Property("EndpointType"); 29 | 30 | b.Property("IsFavourite"); 31 | 32 | b.Property("LastSeen"); 33 | 34 | b.Property("Name"); 35 | 36 | b.Property("_dbAddress") 37 | .HasColumnName("Address"); 38 | 39 | b.HasKey("Id"); 40 | 41 | b.ToTable("Devices"); 42 | }); 43 | 44 | modelBuilder.Entity("CoAPExplorer.Models.DeviceResource", b => 45 | { 46 | b.Property("Id") 47 | .ValueGeneratedOnAdd(); 48 | 49 | b.Property("DeviceId"); 50 | 51 | b.Property("Name"); 52 | 53 | b.Property("_dbContentFormat") 54 | .HasColumnName("ContentFormat"); 55 | 56 | b.Property("_dbUrl") 57 | .HasColumnName("Url"); 58 | 59 | b.HasKey("Id"); 60 | 61 | b.HasIndex("DeviceId"); 62 | 63 | b.ToTable("DeviceResource"); 64 | }); 65 | 66 | modelBuilder.Entity("CoAPExplorer.Models.Message", b => 67 | { 68 | b.Property("Id") 69 | .ValueGeneratedOnAdd(); 70 | 71 | b.Property("Payload"); 72 | 73 | b.Property("_dbCode") 74 | .HasColumnName("Code"); 75 | 76 | b.Property("_dbContentFormat") 77 | .HasColumnName("ContentFormat"); 78 | 79 | b.Property("_dbOptions") 80 | .HasColumnName("Options"); 81 | 82 | b.Property("_dbUrl") 83 | .HasColumnName("Url"); 84 | 85 | b.HasKey("Id"); 86 | 87 | b.ToTable("RecentMessages"); 88 | }); 89 | 90 | modelBuilder.Entity("CoAPExplorer.Models.DeviceResource", b => 91 | { 92 | b.HasOne("CoAPExplorer.Models.Device", "Device") 93 | .WithMany("KnownResources") 94 | .HasForeignKey("DeviceId"); 95 | }); 96 | #pragma warning restore 612, 618 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /Tools/Database/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Database 4 | { 5 | class Program 6 | { 7 | static void Main(string[] args) 8 | { 9 | Console.WriteLine("Hello World!"); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | version: 0.0.1-pre{build} 2 | image: Visual Studio 2017 3 | 4 | configuration: Release 5 | 6 | before_build: 7 | - nuget restore 8 | 9 | build: 10 | verbosity: minimal 11 | 12 | test: off 13 | 14 | artifacts: 15 | - path: src/CoAPExplorer.WPF/bin/$(configuration)/ 16 | name: Windows Binaries 17 | 18 | -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/CoAPExplorer.WPF/App.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/CoAPExplorer.WPF/App.xaml: -------------------------------------------------------------------------------- 1 |  8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 36 | -------------------------------------------------------------------------------- /src/CoAPExplorer.WPF/Themes/DeviceListView.xaml: -------------------------------------------------------------------------------- 1 |  8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 78 | -------------------------------------------------------------------------------- /src/CoAPExplorer.WPF/Views/DeviceNavigationView.xaml: -------------------------------------------------------------------------------- 1 |  13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 47 | 50 | 51 | 52 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 76 | 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /src/CoAPExplorer.WPF/Views/DeviceNavigationView.xaml.cs: -------------------------------------------------------------------------------- 1 | using CoAPExplorer.ViewModels; 2 | using ReactiveUI; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Reactive.Disposables; 7 | using System.Text; 8 | using System.Threading.Tasks; 9 | using System.Windows; 10 | using System.Windows.Controls; 11 | using System.Windows.Data; 12 | using System.Windows.Documents; 13 | using System.Windows.Input; 14 | using System.Windows.Media; 15 | using System.Windows.Media.Imaging; 16 | using System.Windows.Navigation; 17 | using System.Windows.Shapes; 18 | 19 | namespace CoAPExplorer.WPF.Views 20 | { 21 | /// 22 | /// Interaction logic for DeviceNavigationView.xaml 23 | /// 24 | public partial class DeviceNavigationView : UserControl, IViewFor 25 | { 26 | public static readonly DependencyProperty IsOpenProperty = DependencyProperty.Register( 27 | nameof(IsOpen), typeof(bool), typeof(DeviceNavigationView), new PropertyMetadata(true)); 28 | 29 | public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register( 30 | nameof(ViewModel), typeof(DeviceNavigationViewModel), typeof(DeviceNavigationView), new PropertyMetadata(null)); 31 | 32 | public DeviceNavigationViewModel ViewModel { get => GetValue(ViewModelProperty) as DeviceNavigationViewModel; set => SetValue(ViewModelProperty, value); } 33 | object IViewFor.ViewModel { get => ViewModel; set => ViewModel = value as DeviceNavigationViewModel; } 34 | 35 | public bool IsOpen 36 | { 37 | get => (bool)GetValue(IsOpenProperty); 38 | set => SetValue(IsOpenProperty, value); 39 | } 40 | 41 | public DeviceNavigationView() 42 | { 43 | InitializeComponent(); 44 | 45 | //SetBinding(DataContextProperty, new Binding(nameof(ViewModel)) { Mode = BindingMode.OneWay }); 46 | this.WhenActivated(disposables => 47 | { 48 | this.WhenAnyValue(t => t.ViewModel).Subscribe(NewViewModel => DataContext = NewViewModel).DisposeWith(disposables); 49 | 50 | this.BindCommand(ViewModel, 51 | vm => vm.RefreshResourcesCommand, 52 | v => v.RefreshButton, 53 | vm => vm.Device) 54 | .DisposeWith(disposables); 55 | 56 | this.OneWayBind(ViewModel, vm => vm.Resources, v => v.ResourceList.ItemsSource) 57 | .DisposeWith(disposables); 58 | 59 | this.Bind(ViewModel, vm => vm.SelectedResource, v => v.ResourceList.SelectedItem) 60 | .DisposeWith(disposables); 61 | }); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/CoAPExplorer.WPF/Views/DeviceView.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Linq; 4 | using System.Reactive.Disposables; 5 | using System.Reactive.Linq; 6 | using System.Windows; 7 | using System.Windows.Controls; 8 | using System.Windows.Data; 9 | using System.Windows.Documents; 10 | using System.Windows.Input; 11 | using System.Windows.Media; 12 | using System.Windows.Media.Imaging; 13 | using System.Windows.Navigation; 14 | 15 | using CoAPNet; 16 | using CoAPNet.Options; 17 | using ReactiveUI; 18 | 19 | using CoAPExplorer.Models; 20 | using CoAPExplorer.ViewModels; 21 | using CoAPExplorer.WPF.Converters; 22 | 23 | namespace CoAPExplorer.WPF.Views 24 | { 25 | /// 26 | /// Interaction logic for DeviceView.xaml 27 | /// 28 | public partial class DeviceView : UserControl, IViewFor 29 | { 30 | private static readonly BooleanToVisibilityConverter _visibilityConverter = new BooleanToVisibilityConverter(); 31 | 32 | private ReactiveCommand NavigateCommand; 33 | 34 | public DeviceView() 35 | { 36 | InitializeComponent(); 37 | 38 | NavigateCommand = ReactiveCommand.CreateFromObservable( 39 | () => ViewModel.HostScreen.Router.NavigateBack.Execute(), 40 | this.WhenAnyValue(x => x.ViewModel).Select(x => x != null && x.HostScreen != null)); 41 | 42 | NavigateBackButton.Command = NavigateCommand; 43 | 44 | this.WhenActivated(disposables => 45 | { 46 | this.Bind(ViewModel, vm => vm.Message, v => v.Url.SelectedItem, 47 | this.WhenAnyValue(v => v.Url.SelectedItem).Where(i => i != null)) 48 | .DisposeWith(disposables); 49 | 50 | this.OneWayBind(ViewModel, vm => vm.RecentMessages, v => v.Url.ItemsSource) 51 | .DisposeWith(disposables); 52 | 53 | this.BindCommand(ViewModel, vm => vm.SendCommand, v => v.SendButton, vm => vm.Message) 54 | .DisposeWith(disposables); 55 | 56 | this.BindCommand(ViewModel, vm => vm.StopSendingCommand, v => v.StopButton) 57 | .DisposeWith(disposables); 58 | 59 | this.BindCommand(ViewModel, vm=> vm.DuplicateMessageCommand, v => v.DuplicateMessageButton, 60 | ViewModel.WhenAnyValue(vm => vm.Message)) 61 | .DisposeWith(disposables); 62 | 63 | this.OneWayBind(ViewModel, 64 | vm => vm.IsSending, 65 | v => v.StopButton.Visibility, 66 | x => _visibilityConverter.Convert(x, typeof(Visibility), null, CultureInfo.CurrentCulture)) 67 | .DisposeWith(disposables); 68 | 69 | this.OneWayBind(ViewModel, 70 | vm => vm.IsSending, 71 | v => v.SendButton.Visibility, 72 | x => _visibilityConverter.Convert(!x, typeof(Visibility), null, CultureInfo.CurrentCulture)) 73 | .DisposeWith(disposables); 74 | 75 | this.Bind(ViewModel, vm => vm.MessageViewModel, v => v.MessageRequest.ViewModel) 76 | .DisposeWith(disposables); 77 | 78 | this.OneWayBind(ViewModel, 79 | vm => vm.HostScreen.Router.NavigationStack, 80 | v => v.NavigateBackButton.Visibility, 81 | stack => stack.Any() ? Visibility.Visible : Visibility.Collapsed); 82 | 83 | this.OneWayBind(ViewModel, vm => vm.Navigation, v => v.DeviceNavigation.ViewModel) 84 | .DisposeWith(disposables); 85 | 86 | ViewModel.SendCommand 87 | .Subscribe(response => 88 | { 89 | MessageResponse.ViewModel = new MessageViewModel(response); 90 | MessageTabControl.SelectedItem = ReponseTab; 91 | }) 92 | .DisposeWith(disposables); 93 | 94 | Observable.Merge(Url.Events().KeyUp.Where(k => k.Key == Key.Enter).Select(_ => false), 95 | Url.Events().LostKeyboardFocus.Select(_ => false)) 96 | .Subscribe(_ => CreateMessage()); 97 | 98 | }); 99 | } 100 | 101 | private void CreateMessage() 102 | { 103 | if (ViewModel == null) 104 | return; 105 | 106 | if (Url.SelectedItem == null) 107 | { 108 | var message = ViewModel.Message.Clone(); 109 | 110 | if(Uri.TryCreate(Url.Text, UriKind.RelativeOrAbsolute, out var url)) 111 | message.Url = url; 112 | 113 | ViewModel.Message = message; 114 | } 115 | } 116 | 117 | public static DependencyProperty ViewModelProperty = DependencyProperty.Register( 118 | nameof(ViewModel), typeof(DeviceViewModel), typeof(DeviceView), new PropertyMetadata(null)); 119 | 120 | public DeviceViewModel ViewModel { get => GetValue(ViewModelProperty) as DeviceViewModel; set => SetValue(ViewModelProperty, value); } 121 | object IViewFor.ViewModel { get => ViewModel; set => ViewModel = value as DeviceViewModel; } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/CoAPExplorer.WPF/Views/HomeView.xaml: -------------------------------------------------------------------------------- 1 |  15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 🤷‍ 42 | 43 | 44 | 45 | 46 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /src/CoAPExplorer.WPF/Views/HomeView.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reactive.Disposables; 3 | using System.Reactive.Linq; 4 | using System.Windows.Controls; 5 | 6 | using ReactiveUI; 7 | using Splat; 8 | 9 | using CoAPExplorer.Models; 10 | using CoAPExplorer.ViewModels; 11 | 12 | namespace CoAPExplorer.WPF.Views 13 | { 14 | /// 15 | /// Interaction logic for HomeView.xaml 16 | /// 17 | public partial class HomeView : Page, IViewFor 18 | { 19 | public IScreen Router { get; } 20 | 21 | public HomeViewModel ViewModel { get; set; } 22 | 23 | public HomeView(IScreen router = null) 24 | { 25 | InitializeComponent(); 26 | 27 | this.WhenActivated(disposables => 28 | { 29 | this.WhenAnyValue(x => x.ViewModel) 30 | .Subscribe(vm => DataContext = vm) 31 | .DisposeWith(disposables); 32 | }); 33 | } 34 | 35 | 36 | object IViewFor.ViewModel { get => ViewModel; set => ViewModel = value as HomeViewModel; } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/CoAPExplorer.WPF/Views/MessageResponseView.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Globalization; 3 | using System.Reactive.Linq; 4 | using System.Reactive.Disposables; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | using System.Windows; 8 | using System.Windows.Controls; 9 | using System.Windows.Data; 10 | using System.Windows.Documents; 11 | using System.Windows.Input; 12 | using System.Windows.Media; 13 | using System.Windows.Media.Imaging; 14 | using System.Windows.Navigation; 15 | using System.Windows.Shapes; 16 | 17 | using ReactiveUI; 18 | 19 | using CoAPExplorer.Models; 20 | using CoAPExplorer.ViewModels; 21 | using CoAPExplorer.WPF.Converters; 22 | using System.Text.RegularExpressions; 23 | using System.Linq; 24 | using CoAPExplorer.WPF.Services; 25 | 26 | namespace CoAPExplorer.WPF.Views 27 | { 28 | 29 | /// 30 | /// Interaction logic for MessageResponseView.xaml 31 | /// 32 | public partial class MessageResponseView : UserControl, IViewFor 33 | { 34 | private CompositeDisposable _viewModelDisposables; 35 | 36 | private static readonly HextoAsciiConverter _hextoAsciiConverter = new HextoAsciiConverter(); 37 | 38 | public MessageResponseView() 39 | { 40 | InitializeComponent(); 41 | 42 | this.WhenActivated(disposables => 43 | { 44 | this.WhenAnyValue(v => v.ViewModel) 45 | .Subscribe(NewViewModel => 46 | { 47 | _viewModelDisposables?.Dispose(); 48 | _viewModelDisposables = new CompositeDisposable(); 49 | 50 | if (NewViewModel == null) 51 | return; 52 | 53 | this.OneWayBind(NewViewModel, vm => vm.MessageId, v => v.MessageIdTextBox.Text) 54 | .DisposeWith(_viewModelDisposables); 55 | 56 | this.OneWayBind(NewViewModel, vm => vm.Token, v => v.MessageToken.Text, 57 | x => _hextoAsciiConverter.Convert(x, typeof(string), 8, CultureInfo.CurrentCulture)) 58 | .DisposeWith(_viewModelDisposables); 59 | 60 | this.OneWayBind(NewViewModel, vm => vm.Code, v => v.MessageCodeTextBox.Text, 61 | x => Consts.MessageCodes.SingleOrDefault(c => c.Item2 == x)?.Item1 ?? x.ToString()) 62 | .DisposeWith(_viewModelDisposables); 63 | 64 | this.OneWayBind(NewViewModel, vm => vm.ContentFormat, v => v.ContentTypeTextBox.Text, 65 | x => Consts.ContentTypes.SingleOrDefault(c => c.Item2?.Value == x.Value)?.Item1 ?? $"{x.Value} - (unknown)") 66 | .DisposeWith(_viewModelDisposables); 67 | 68 | this.Bind(NewViewModel, vm => vm.Options, v => v.OptionsList.Options) 69 | .DisposeWith(_viewModelDisposables); 70 | 71 | this.OneWayBind(NewViewModel, vm => vm.Payload, v => v.MessageTextBox.Text) 72 | .DisposeWith(_viewModelDisposables); 73 | 74 | this.WhenAnyValue(v => v.DisplayUnicode.IsSelected) 75 | .InvokeCommand(NewViewModel, vm => vm.EscapePayload) 76 | .DisposeWith(_viewModelDisposables); 77 | 78 | this.OneWayBind(NewViewModel, vm => vm.FormattedPayload, v => v.FormattedTextEditor.Text) 79 | .DisposeWith(_viewModelDisposables); 80 | 81 | NewViewModel.WhenAnyValue(vm => vm.ContentFormat) 82 | .Select(cf => CoapFormatHighlightingManager.Default.GetDefinition(cf)) 83 | .Subscribe(d => FormattedTextEditor.SyntaxHighlighting = d) 84 | .DisposeWith(_viewModelDisposables); 85 | }) 86 | .DisposeWith(disposables); 87 | }); 88 | 89 | } 90 | 91 | public static DependencyProperty ViewModelProperty = DependencyProperty.Register( 92 | nameof(ViewModel), typeof(MessageViewModel), typeof(MessageResponseView), new PropertyMetadata(null)); 93 | 94 | public MessageViewModel ViewModel { get => GetValue(ViewModelProperty) as MessageViewModel; set => SetValue(ViewModelProperty, value); } 95 | object IViewFor.ViewModel { get => ViewModel; set => ViewModel = value as MessageViewModel; } 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/CoAPExplorer.WPF/Views/NavigationView.xaml: -------------------------------------------------------------------------------- 1 |  12 | 13 | 14 | 15 | 16 | 23 | 24 | 25 | 27 | 28 | 29 | 30 | 33 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/CoAPExplorer.WPF/Views/NavigationView.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel; 3 | using System.Linq; 4 | using System.Reactive; 5 | using System.Reactive.Disposables; 6 | using System.Reactive.Linq; 7 | using System.Windows; 8 | using System.Windows.Controls; 9 | 10 | using CoAPExplorer.Models; 11 | using CoAPExplorer.ViewModels; 12 | using ReactiveUI; 13 | 14 | namespace CoAPExplorer.WPF.Views 15 | { 16 | /// 17 | /// Interaction logic for NavigationView.xaml 18 | /// 19 | public partial class NavigationView : UserControl, IViewFor 20 | { 21 | public static readonly DependencyProperty IsOpenProperty = DependencyProperty.Register( 22 | nameof(IsOpen), typeof(bool), typeof(NavigationView), new PropertyMetadata(true)); 23 | 24 | public static DependencyProperty ViewModelProperty = DependencyProperty.Register( 25 | nameof(ViewModel), typeof(NavigationViewModel), typeof(NavigationView), new PropertyMetadata(null)); 26 | 27 | public bool IsOpen 28 | { 29 | get => (bool)GetValue(IsOpenProperty); 30 | set => SetValue(IsOpenProperty, value); 31 | } 32 | 33 | 34 | public NavigationViewModel ViewModel { get => GetValue(ViewModelProperty) as NavigationViewModel; set => SetValue(ViewModelProperty, value); } 35 | object IViewFor.ViewModel { get => ViewModel; set => ViewModel = value as NavigationViewModel; } 36 | 37 | public NavigationView() 38 | { 39 | InitializeComponent(); 40 | 41 | //CollapseNavigation = ReactiveCommand.Create(() => IsOpen = false); 42 | 43 | #if DEBUG 44 | if (DesignerProperties.GetIsInDesignMode(this)) 45 | IsOpen = true; 46 | #endif 47 | 48 | this.WhenActivated(disposables => 49 | { 50 | this.Bind(ViewModel, vm => vm.IsOpen, v => v.IsOpen) 51 | .DisposeWith(disposables); 52 | 53 | this.OneWayBind(ViewModel, vm => vm.NavigationItems, v => v.NaigationList.ItemsSource) 54 | .DisposeWith(disposables); 55 | 56 | this.Bind(ViewModel, vm => vm.SelectedNavigationItem, v => v.NaigationList.SelectedItem) 57 | .DisposeWith(disposables); 58 | 59 | //Observable.FromEventPattern( 60 | // h => NaigationList.SelectionChanged += h, 61 | // h => NaigationList.SelectionChanged -= h) 62 | // .SelectMany(x => x.EventArgs.AddedItems.Cast()) 63 | // .Select(n => Observable.Return(Unit.Default) 64 | // .InvokeCommand(n.Command) 65 | // .DisposeWith(disposables)) 66 | // .Subscribe() 67 | // .DisposeWith(disposables); 68 | }); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/CoAPExplorer.WPF/Views/RecentDevicesView.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reactive; 3 | using System.Reactive.Linq; 4 | using System.Linq; 5 | using System.Windows.Controls; 6 | using System.Reactive.Disposables; 7 | 8 | using MaterialDesignThemes.Wpf; 9 | using ReactiveUI; 10 | 11 | using CoAPExplorer.ViewModels; 12 | using System.Windows; 13 | using System.Windows.Input; 14 | 15 | namespace CoAPExplorer.WPF.Views 16 | { 17 | /// 18 | /// Interaction logic for RecentDevicesView.xaml 19 | /// 20 | public partial class RecentDevicesView : UserControl, IViewFor 21 | { 22 | private static readonly DependencyProperty SearchCommandProperty = DependencyProperty.Register( 23 | nameof(SearchCommand), typeof(ReactiveCommand), typeof(RecentDevicesView), new PropertyMetadata(default(ReactiveCommand))); 24 | 25 | private ReactiveCommand SearchCommand { get => GetValue(SearchCommandProperty) as ReactiveCommand; set => SetValue(SearchCommandProperty, value); } 26 | 27 | private static readonly DependencyProperty CloseSearchCommandProperty = DependencyProperty.Register( 28 | nameof(CloseSearchCommand), typeof(ReactiveCommand), typeof(RecentDevicesView), new PropertyMetadata(default(ReactiveCommand))); 29 | 30 | private ReactiveCommand CloseSearchCommand { get => GetValue(CloseSearchCommandProperty) as ReactiveCommand; set => SetValue(CloseSearchCommandProperty, value); } 31 | 32 | public RecentDevicesView() 33 | { 34 | InitializeComponent(); 35 | 36 | SearchCommand = ReactiveCommand.Create(() => 37 | { 38 | AppBarTransistioner.SelectedItem = SearchTransistionState; 39 | SearchTextBox.Focus(); 40 | }); 41 | 42 | CloseSearchCommand = ReactiveCommand.Create(() => 43 | { 44 | if (ViewModel != null) 45 | ViewModel.SearchTerms = string.Empty; 46 | 47 | MaterialDesignThemes.Wpf.Transitions.Transitioner.MoveFirstCommand.Execute(Unit.Default, SearchTextBox); 48 | }); 49 | 50 | this.WhenActivated(disposables => 51 | { 52 | this.OneWayBind(ViewModel, vm => vm.FilteredDevices, v => v.DeviceListView.ItemsSource) 53 | .DisposeWith(disposables); 54 | 55 | this.WhenAnyValue(v => v.DeviceListView.SelectedItem) 56 | .Select(x => x as DeviceViewModel) 57 | .Where(x => x != null) 58 | .InvokeCommand(this, v => v.ViewModel.OpenDeviceCommand) 59 | .DisposeWith(disposables); 60 | 61 | this.WhenAnyValue(x => x.ViewModel) 62 | .Where(x => x != null) 63 | .Subscribe(vm => vm.AddDeviceCommand.Subscribe(ndvm => 64 | { 65 | var view = ViewLocator.Current.ResolveView(ndvm); 66 | view.ViewModel = ndvm; 67 | 68 | DialogHost.Show(view); 69 | }) 70 | .DisposeWith(disposables)) 71 | .DisposeWith(disposables); 72 | 73 | this.Bind(ViewModel, vm => vm.SearchTerms, v => v.SearchTextBox.Text) 74 | .DisposeWith(disposables); 75 | 76 | this.SearchTextBox.Events() 77 | .KeyUp.Where(k => k.Key == Key.Enter) 78 | .Select(_ => Unit.Default).InvokeCommand(this, v => v.ViewModel.NavigateToUriCommand) 79 | .DisposeWith(disposables); 80 | 81 | this.SearchTextBox.Events() 82 | .KeyUp.Where(k => k.Key == Key.Escape) 83 | .Select(_ => Unit.Default).InvokeCommand(this, v => v.CloseSearchCommand) 84 | .DisposeWith(disposables); 85 | 86 | this.BindCommand(ViewModel, vm => vm.NavigateToUriCommand, v => v.NavigateToButton) 87 | .DisposeWith(disposables); 88 | 89 | this.OneWayBind(ViewModel, 90 | vm => vm.IsSearchValidUri, 91 | v => v.NavigateToButton.Visibility, 92 | x => x ? Visibility.Visible : Visibility.Collapsed) 93 | .DisposeWith(disposables); 94 | 95 | this.BindCommand(ViewModel, vm => vm.AddDeviceCommand, v => v.AddButton, nameof(AddButton.ToggleCheckedContentClick)) 96 | .DisposeWith(disposables); 97 | }); 98 | } 99 | 100 | public readonly static DependencyProperty ViewModelProperty = DependencyProperty.Register( 101 | nameof(ViewModel), typeof(RecentDevicesViewModel), typeof(RecentDevicesView), new PropertyMetadata(null)); 102 | 103 | public RecentDevicesViewModel ViewModel { get => (RecentDevicesViewModel)GetValue(ViewModelProperty); set => SetValue(ViewModelProperty, value); } 104 | 105 | object IViewFor.ViewModel { get => ViewModel; set => ViewModel = value as RecentDevicesViewModel; } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/CoAPExplorer.WPF/Views/SearchView.xaml.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Reactive; 6 | using System.Reactive.Disposables; 7 | using System.Reactive.Linq; 8 | using System.Text; 9 | using System.Threading.Tasks; 10 | using System.Windows; 11 | using System.Windows.Controls; 12 | using System.Windows.Data; 13 | using System.Windows.Documents; 14 | using System.Windows.Input; 15 | using System.Windows.Media; 16 | using System.Windows.Media.Imaging; 17 | using System.Windows.Navigation; 18 | using System.Windows.Shapes; 19 | using CoAPExplorer.Models; 20 | using CoAPExplorer.ViewModels; 21 | using ReactiveUI; 22 | 23 | namespace CoAPExplorer.WPF.Views 24 | { 25 | /// 26 | /// Interaction logic for SearchView.xaml 27 | /// 28 | public partial class SearchView : UserControl, IViewFor 29 | { 30 | 31 | public SearchView() 32 | { 33 | InitializeComponent(); 34 | 35 | this.WhenActivated((CompositeDisposable disposables) => 36 | { 37 | var visibilityConverter = new BooleanToVisibilityConverter(); 38 | 39 | this.OneWayBind(ViewModel, vm => vm.Devices, v => v.DeviceList.ItemsSource) 40 | .DisposeWith(disposables); 41 | 42 | this.WhenAnyValue(v => v.DeviceList.SelectedItem) 43 | .Select(x => x as DeviceViewModel) 44 | .Where(x => x != null) 45 | .InvokeCommand(this, v => v.ViewModel.OpenDeviceCommand) 46 | .DisposeWith(disposables); 47 | 48 | this.Bind(ViewModel, vm => vm.SearchUrl, v => v.SearchUrl.Text) 49 | .DisposeWith(disposables); 50 | 51 | this.OneWayBind(ViewModel, 52 | vm => vm.IsSearching, 53 | v => v.GoButton.Visibility, 54 | x => visibilityConverter.Convert(!x, typeof(Visibility), null, CultureInfo.CurrentCulture)) 55 | .DisposeWith(disposables); 56 | 57 | this.BindCommand(ViewModel, vm => vm.SearchCommand, v => v.GoButton) 58 | .DisposeWith(disposables); 59 | 60 | 61 | this.OneWayBind(ViewModel, 62 | vm => vm.IsSearching, 63 | v => v.StopButton.Visibility, 64 | x => visibilityConverter.Convert(x, typeof(Visibility), null, CultureInfo.CurrentCulture)) 65 | .DisposeWith(disposables); 66 | 67 | this.BindCommand(ViewModel, vm => vm.StopCommand, v => v.StopButton) 68 | .DisposeWith(disposables); 69 | 70 | this.OneWayBind(ViewModel, 71 | vm => vm.IsSearching, 72 | v => v.SearchProgress.Visibility, 73 | x => visibilityConverter.Convert(x, typeof(Visibility), null, CultureInfo.CurrentCulture)) 74 | .DisposeWith(disposables); 75 | 76 | this.OneWayBind(ViewModel, vm => vm.IsSearching, v => v.FilterPanel.IsEnabled, x => !x) 77 | .DisposeWith(disposables); 78 | }); 79 | } 80 | 81 | #region IViewFor Boilerplate 82 | 83 | public readonly static DependencyProperty ViewModelProperty = DependencyProperty.Register( 84 | nameof(ViewModel), typeof(SearchViewModel), typeof(SearchView), new PropertyMetadata(null)); 85 | 86 | object IViewFor.ViewModel { get => ViewModel; set => ViewModel = value as SearchViewModel; } 87 | 88 | public SearchViewModel ViewModel { get => (SearchViewModel)GetValue(ViewModelProperty); set => SetValue(ViewModelProperty, value); } 89 | 90 | #endregion 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/CoAPExplorer/App.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Reactive; 6 | using System.Reactive.Linq; 7 | using System.Reactive.Subjects; 8 | using System.Text; 9 | 10 | using CoAPNet; 11 | using CoAPNet.Udp; 12 | using ReactiveUI; 13 | using Splat; 14 | 15 | using CoAPExplorer.Extensions; 16 | using CoAPExplorer.Models; 17 | using CoAPExplorer.Services; 18 | using CoAPExplorer.ViewModels; 19 | 20 | 21 | namespace CoAPExplorer 22 | { 23 | public class App 24 | { 25 | public string DataPath { get; } 26 | 27 | private ISubject _toastNotifications 28 | = new Subject(); 29 | 30 | public IObservable ToastNotifications => _toastNotifications; 31 | 32 | public IMutableDependencyResolver Locator => global::Splat.Locator.CurrentMutable; 33 | 34 | /// 35 | /// Logs the exception to the applications log directory and invokes the exception event for displaying to the user. 36 | /// 37 | /// 38 | public static void LogException(Exception exception) 39 | { 40 | if (exception == null) 41 | return; 42 | 43 | var app = Splat.Locator.Current.GetService(); 44 | 45 | // TODO: Fire an event which will display a toast of the recently received exception. 46 | // TODO: Create a view (seperate window?) for viewing all exceptions 47 | 48 | var logPath = new DirectoryInfo(Path.Combine(app.DataPath, "logs")); 49 | 50 | if (!logPath.Exists) 51 | logPath.Create(); 52 | 53 | var timestamp = DateTime.UtcNow.ToString("yyyy-MM-ddTHH-mm-ss", System.Globalization.CultureInfo.InvariantCulture); 54 | var baseFileName = $"{timestamp}-Error-{exception.GetType().Name}"; 55 | 56 | var attempt = 1; 57 | var filename = Path.Combine(app.DataPath, "logs", $"{baseFileName}.log"); 58 | while(File.Exists(filename)) 59 | filename = Path.Combine(app.DataPath, "logs", $"{baseFileName}.{attempt++}.log"); 60 | 61 | using (var log = new StreamWriter(filename, false, Encoding.UTF8)) 62 | log.Write(exception.ToString()); 63 | 64 | var message = "An error has occured."; 65 | #if DEBUG 66 | // Only display exception details in the UI for debug builds 67 | message = $"{exception.GetType().Name}: {exception.Message}."; 68 | #endif 69 | app._toastNotifications.OnNext(new ToastNotification(message, ToastNotificationType.Error, ("Show", OpenLogFile(filename)))); 70 | } 71 | 72 | private static ReactiveCommand OpenLogFile(string filename) 73 | { 74 | return ReactiveCommand.Create(() => 75 | { 76 | var process = Process.Start(filename); 77 | }); 78 | } 79 | 80 | public App(string dataPath) 81 | { 82 | DataPath = dataPath; 83 | 84 | #if DEBUG 85 | // Debug logging 86 | Locator.RegisterConstant(new MyDebugLogger { Level = LogLevel.Debug }); 87 | #endif 88 | 89 | // Register logger for all require generic uses of Microsoft.Extensions.Logging.ILogger 90 | //services.RegisterLogger() 91 | // .RegisterLogger() 92 | // .RegisterLogger(); 93 | 94 | // Ensure coap related schemas are supported 95 | CoapStyleUriParser.Register(); 96 | 97 | // App-wide services 98 | Locator 99 | .RegisterLogger(); 100 | 101 | Locator.Register(() => new DiscoveryService()); 102 | } 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/CoAPExplorer/CoAPExplorer.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0 5 | latest 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/CoAPExplorer/CoapIcon.cs: -------------------------------------------------------------------------------- 1 | namespace CoAPExplorer 2 | { 3 | public enum CoapExplorerIcon 4 | { 5 | None, 6 | Settings, 7 | Search, 8 | Favouriate, 9 | Recent 10 | } 11 | } -------------------------------------------------------------------------------- /src/CoAPExplorer/Database/CoapExplorerContext.cs: -------------------------------------------------------------------------------- 1 | using CoAPExplorer.Models; 2 | using Microsoft.EntityFrameworkCore; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Text; 6 | 7 | namespace CoAPExplorer.Database 8 | { 9 | public class CoapExplorerContext : DbContext 10 | { 11 | private readonly string _databasePath; 12 | 13 | public DbSet Devices { get; set; } 14 | 15 | public DbSet RecentMessages { get; set; } 16 | 17 | public string DatabasePath => _databasePath; 18 | 19 | public CoapExplorerContext(DbContextOptions options) 20 | :base(options) 21 | { } 22 | 23 | public CoapExplorerContext(string databasePath) 24 | { 25 | _databasePath = databasePath; 26 | } 27 | 28 | protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) 29 | { 30 | if(!string.IsNullOrEmpty(_databasePath)) 31 | optionsBuilder.UseSqlite($"Filename={_databasePath}"); 32 | 33 | base.OnConfiguring(optionsBuilder); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/CoAPExplorer/Database/CoapOptionSerialiser.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using CoAPNet; 5 | using Newtonsoft.Json; 6 | using Newtonsoft.Json.Linq; 7 | using Newtonsoft.Json.Serialization; 8 | 9 | namespace CoAPExplorer.Database 10 | { 11 | //[JsonConverter(typeof(StringFlagEnumConverter))] 12 | public class CoapOptionConverter : JsonConverter 13 | { 14 | // TODO: Support custom option factory 15 | private readonly CoAPNet.Options.OptionFactory _factory = CoAPNet.Options.OptionFactory.Default; 16 | 17 | public override CoapOption ReadJson(JsonReader reader, Type objectType, CoapOption existingValue, bool hasExistingValue, JsonSerializer serializer) 18 | { 19 | if (reader.TokenType == JsonToken.Null) 20 | return null; 21 | 22 | if (reader.TokenType != JsonToken.StartObject) 23 | return null; 24 | 25 | byte[] data = new byte[] { }; 26 | int? number = 0; 27 | 28 | reader.Read(); 29 | 30 | while (reader.TokenType != JsonToken.EndObject) 31 | { 32 | if (reader.Path.EndsWith(".n")) 33 | number = reader.ReadAsInt32(); 34 | else if(reader.Path.EndsWith(".d")) 35 | data = reader.ReadAsBytes(); 36 | else 37 | reader.Read(); 38 | 39 | reader.Read(); 40 | } 41 | 42 | return _factory.Create(number.Value, data); 43 | } 44 | 45 | public override void WriteJson(JsonWriter writer, CoapOption value, JsonSerializer serializer) 46 | { 47 | if (value == null) 48 | { 49 | writer.WriteNull(); 50 | return; 51 | } 52 | 53 | writer.WriteStartObject(); 54 | 55 | writer.WritePropertyName("n"); 56 | writer.WriteValue(value.OptionNumber); 57 | 58 | writer.WritePropertyName("d"); 59 | writer.WriteValue(value.GetBytes()); 60 | 61 | writer.WriteEndObject(); 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/CoAPExplorer/Database/Migrations/20180516004057_Initial.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | using CoAPExplorer; 3 | using CoAPExplorer.Database; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using Microsoft.EntityFrameworkCore.Migrations; 8 | using Microsoft.EntityFrameworkCore.Storage; 9 | using Microsoft.EntityFrameworkCore.Storage.Internal; 10 | using System; 11 | 12 | namespace CoAPExplorer.Database.Migrations 13 | { 14 | [DbContext(typeof(CoapExplorerContext))] 15 | [Migration("20180516004057_Initial")] 16 | partial class Initial 17 | { 18 | protected override void BuildTargetModel(ModelBuilder modelBuilder) 19 | { 20 | #pragma warning disable 612, 618 21 | modelBuilder 22 | .HasAnnotation("ProductVersion", "2.0.2-rtm-10011"); 23 | 24 | modelBuilder.Entity("CoAPExplorer.Models.Device", b => 25 | { 26 | b.Property("Id") 27 | .ValueGeneratedOnAdd(); 28 | 29 | b.Property("EndpointType"); 30 | 31 | b.Property("IsFavourite"); 32 | 33 | b.Property("LastSeen"); 34 | 35 | b.Property("Name"); 36 | 37 | b.Property("_dbAddress") 38 | .HasColumnName("Address"); 39 | 40 | b.HasKey("Id"); 41 | 42 | b.ToTable("Devices"); 43 | }); 44 | 45 | modelBuilder.Entity("CoAPExplorer.Models.DeviceResource", b => 46 | { 47 | b.Property("Id") 48 | .ValueGeneratedOnAdd(); 49 | 50 | b.Property("DeviceId"); 51 | 52 | b.Property("Name"); 53 | 54 | b.Property("_dbContentFormat") 55 | .HasColumnName("ContentFormat"); 56 | 57 | b.Property("_dbUrl") 58 | .HasColumnName("Url"); 59 | 60 | b.HasKey("Id"); 61 | 62 | b.HasIndex("DeviceId"); 63 | 64 | b.ToTable("DeviceResource"); 65 | }); 66 | 67 | modelBuilder.Entity("CoAPExplorer.Models.Message", b => 68 | { 69 | b.Property("Id") 70 | .ValueGeneratedOnAdd(); 71 | 72 | b.Property("Payload"); 73 | 74 | b.Property("_dbCode") 75 | .HasColumnName("Code"); 76 | 77 | b.Property("_dbContentFormat") 78 | .HasColumnName("ContentFormat"); 79 | 80 | b.Property("_dbOptions") 81 | .HasColumnName("Options"); 82 | 83 | b.Property("_dbUrl") 84 | .HasColumnName("Url"); 85 | 86 | b.HasKey("Id"); 87 | 88 | b.ToTable("RecentMessages"); 89 | }); 90 | 91 | modelBuilder.Entity("CoAPExplorer.Models.DeviceResource", b => 92 | { 93 | b.HasOne("CoAPExplorer.Models.Device", "Device") 94 | .WithMany("KnownResources") 95 | .HasForeignKey("DeviceId"); 96 | }); 97 | #pragma warning restore 612, 618 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/CoAPExplorer/Database/Migrations/20180516004057_Initial.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.EntityFrameworkCore.Migrations; 2 | using System; 3 | using System.Collections.Generic; 4 | 5 | namespace CoAPExplorer.Database.Migrations 6 | { 7 | public partial class Initial : Migration 8 | { 9 | protected override void Up(MigrationBuilder migrationBuilder) 10 | { 11 | migrationBuilder.CreateTable( 12 | name: "Devices", 13 | columns: table => new 14 | { 15 | Id = table.Column(nullable: false) 16 | .Annotation("Sqlite:Autoincrement", true), 17 | EndpointType = table.Column(nullable: false), 18 | IsFavourite = table.Column(nullable: false), 19 | LastSeen = table.Column(nullable: false), 20 | Name = table.Column(nullable: true), 21 | Address = table.Column(nullable: true) 22 | }, 23 | constraints: table => 24 | { 25 | table.PrimaryKey("PK_Devices", x => x.Id); 26 | }); 27 | 28 | migrationBuilder.CreateTable( 29 | name: "RecentMessages", 30 | columns: table => new 31 | { 32 | Id = table.Column(nullable: false) 33 | .Annotation("Sqlite:Autoincrement", true), 34 | Payload = table.Column(nullable: true), 35 | Code = table.Column(nullable: true), 36 | ContentFormat = table.Column(nullable: true), 37 | Options = table.Column(nullable: true), 38 | Url = table.Column(nullable: true) 39 | }, 40 | constraints: table => 41 | { 42 | table.PrimaryKey("PK_RecentMessages", x => x.Id); 43 | }); 44 | 45 | migrationBuilder.CreateTable( 46 | name: "DeviceResource", 47 | columns: table => new 48 | { 49 | Id = table.Column(nullable: false) 50 | .Annotation("Sqlite:Autoincrement", true), 51 | DeviceId = table.Column(nullable: true), 52 | Name = table.Column(nullable: true), 53 | ContentFormat = table.Column(nullable: true), 54 | Url = table.Column(nullable: true) 55 | }, 56 | constraints: table => 57 | { 58 | table.PrimaryKey("PK_DeviceResource", x => x.Id); 59 | table.ForeignKey( 60 | name: "FK_DeviceResource_Devices_DeviceId", 61 | column: x => x.DeviceId, 62 | principalTable: "Devices", 63 | principalColumn: "Id", 64 | onDelete: ReferentialAction.Restrict); 65 | }); 66 | 67 | migrationBuilder.CreateIndex( 68 | name: "IX_DeviceResource_DeviceId", 69 | table: "DeviceResource", 70 | column: "DeviceId"); 71 | } 72 | 73 | protected override void Down(MigrationBuilder migrationBuilder) 74 | { 75 | migrationBuilder.DropTable( 76 | name: "DeviceResource"); 77 | 78 | migrationBuilder.DropTable( 79 | name: "RecentMessages"); 80 | 81 | migrationBuilder.DropTable( 82 | name: "Devices"); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/CoAPExplorer/Database/Migrations/CoapExplorerContextModelSnapshot.cs: -------------------------------------------------------------------------------- 1 | // 2 | using CoAPExplorer; 3 | using CoAPExplorer.Database; 4 | using Microsoft.EntityFrameworkCore; 5 | using Microsoft.EntityFrameworkCore.Infrastructure; 6 | using Microsoft.EntityFrameworkCore.Metadata; 7 | using Microsoft.EntityFrameworkCore.Migrations; 8 | using Microsoft.EntityFrameworkCore.Storage; 9 | using Microsoft.EntityFrameworkCore.Storage.Internal; 10 | using System; 11 | 12 | namespace CoAPExplorer.Database.Migrations 13 | { 14 | [DbContext(typeof(CoapExplorerContext))] 15 | partial class CoapExplorerContextModelSnapshot : ModelSnapshot 16 | { 17 | protected override void BuildModel(ModelBuilder modelBuilder) 18 | { 19 | #pragma warning disable 612, 618 20 | modelBuilder 21 | .HasAnnotation("ProductVersion", "2.0.2-rtm-10011"); 22 | 23 | modelBuilder.Entity("CoAPExplorer.Models.Device", b => 24 | { 25 | b.Property("Id") 26 | .ValueGeneratedOnAdd(); 27 | 28 | b.Property("EndpointType"); 29 | 30 | b.Property("IsFavourite"); 31 | 32 | b.Property("LastSeen"); 33 | 34 | b.Property("Name"); 35 | 36 | b.Property("_dbAddress") 37 | .HasColumnName("Address"); 38 | 39 | b.HasKey("Id"); 40 | 41 | b.ToTable("Devices"); 42 | }); 43 | 44 | modelBuilder.Entity("CoAPExplorer.Models.DeviceResource", b => 45 | { 46 | b.Property("Id") 47 | .ValueGeneratedOnAdd(); 48 | 49 | b.Property("DeviceId"); 50 | 51 | b.Property("Name"); 52 | 53 | b.Property("_dbContentFormat") 54 | .HasColumnName("ContentFormat"); 55 | 56 | b.Property("_dbUrl") 57 | .HasColumnName("Url"); 58 | 59 | b.HasKey("Id"); 60 | 61 | b.HasIndex("DeviceId"); 62 | 63 | b.ToTable("DeviceResource"); 64 | }); 65 | 66 | modelBuilder.Entity("CoAPExplorer.Models.Message", b => 67 | { 68 | b.Property("Id") 69 | .ValueGeneratedOnAdd(); 70 | 71 | b.Property("Payload"); 72 | 73 | b.Property("_dbCode") 74 | .HasColumnName("Code"); 75 | 76 | b.Property("_dbContentFormat") 77 | .HasColumnName("ContentFormat"); 78 | 79 | b.Property("_dbOptions") 80 | .HasColumnName("Options"); 81 | 82 | b.Property("_dbUrl") 83 | .HasColumnName("Url"); 84 | 85 | b.HasKey("Id"); 86 | 87 | b.ToTable("RecentMessages"); 88 | }); 89 | 90 | modelBuilder.Entity("CoAPExplorer.Models.DeviceResource", b => 91 | { 92 | b.HasOne("CoAPExplorer.Models.Device", "Device") 93 | .WithMany("KnownResources") 94 | .HasForeignKey("DeviceId"); 95 | }); 96 | #pragma warning restore 612, 618 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/CoAPExplorer/EndpointType.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace CoAPExplorer 4 | { 5 | public enum EndpointType 6 | { 7 | [Display(Name = "None")] 8 | None, 9 | [Display(Name = "UDP")] 10 | Udp 11 | } 12 | } -------------------------------------------------------------------------------- /src/CoAPExplorer/Extensions/CoapMessageExtensions.cs: -------------------------------------------------------------------------------- 1 | using CoAPExplorer.Models; 2 | using CoAPNet; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Collections.ObjectModel; 6 | using System.Linq; 7 | using System.Text; 8 | 9 | namespace CoAPExplorer.Extensions 10 | { 11 | internal static class CoapMessageExtensions 12 | { 13 | public static CoapMessage ToCoapMessage(this Message message) 14 | { 15 | var coapMessage = new CoapMessage 16 | { 17 | Id = message.MessageId, 18 | Token = message.Token ?? new byte[] { }, 19 | Code = message.Code, 20 | Type = CoapMessageType.Confirmable, 21 | }; 22 | coapMessage.SetUri(message.Url, UriComponents.PathAndQuery); 23 | 24 | if ((message.Code.IsRequest()) && message.ContentFormat != null) 25 | { 26 | coapMessage.Options.Add(new CoAPNet.Options.ContentFormat(message.ContentFormat)); 27 | coapMessage.Payload = message.Payload; 28 | } 29 | 30 | foreach (var option in message.Options) 31 | coapMessage.Options.Add(option); 32 | 33 | return coapMessage; 34 | } 35 | 36 | public static Message ToMessage(this CoapMessage coapMessage) 37 | { 38 | var contentTypeOption 39 | = coapMessage.Options.FirstOrDefault(o => o.OptionNumber == CoapRegisteredOptionNumber.ContentFormat) 40 | as CoAPNet.Options.ContentFormat; 41 | 42 | var message = new Message 43 | { 44 | MessageId = coapMessage.Id, 45 | Token = coapMessage.Token, 46 | Code = coapMessage.Code, 47 | 48 | ContentFormat = contentTypeOption?.MediaType, 49 | 50 | Url = coapMessage.GetUri(), 51 | Payload = coapMessage.Payload, 52 | }; 53 | 54 | message.Options = new ObservableCollection( 55 | coapMessage.Options.Where(o => o.OptionNumber != CoapRegisteredOptionNumber.ContentFormat)); 56 | 57 | return message; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/CoAPExplorer/Extensions/EnumExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Reflection; 5 | using System.Text; 6 | 7 | namespace CoAPExplorer.Extensions 8 | { 9 | public static class EnumExtensions 10 | { 11 | public static string GetDisplayValue(this Enum value) 12 | { 13 | var fieldInfo = value.GetType().GetField(value.ToString()); 14 | 15 | var descriptionAttributes = fieldInfo.GetCustomAttributes( 16 | typeof(DisplayAttribute), false) as DisplayAttribute[]; 17 | 18 | if (descriptionAttributes[0].ResourceType != null) 19 | { 20 | foreach (PropertyInfo staticProperty in descriptionAttributes[0].ResourceType.GetProperties(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public)) 21 | { 22 | if (staticProperty.PropertyType == typeof(System.Resources.ResourceManager)) 23 | { 24 | System.Resources.ResourceManager resourceManager = (System.Resources.ResourceManager)staticProperty.GetValue(null, null); 25 | return resourceManager.GetString(descriptionAttributes[0].Name); 26 | } 27 | } 28 | 29 | return descriptionAttributes[0].Name; // Fallback with the key name 30 | } 31 | 32 | if (descriptionAttributes == null) return string.Empty; 33 | return (descriptionAttributes.Length > 0) ? descriptionAttributes[0].Name : value.ToString(); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/CoAPExplorer/FormattedTextException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CoAPExplorer 4 | { 5 | public class FormattedTextException : Exception 6 | { 7 | public FormattedTextException(string message, int line, int offset) 8 | : base(message) 9 | { 10 | Line = line; 11 | Offset = offset; 12 | } 13 | 14 | public FormattedTextException(string message, int line, int offset, Exception innerException) 15 | : base(message, innerException) 16 | { 17 | Line = line; 18 | Offset = offset; 19 | } 20 | 21 | public int Line { get; } 22 | 23 | public int Offset { get; } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/CoAPExplorer/Models/Device.cs: -------------------------------------------------------------------------------- 1 | using CoAPExplorer.Services; 2 | using CoAPNet; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Collections.ObjectModel; 6 | using System.ComponentModel.DataAnnotations; 7 | using System.ComponentModel.DataAnnotations.Schema; 8 | using System.Threading.Tasks; 9 | 10 | namespace CoAPExplorer.Models 11 | { 12 | public class Device 13 | { 14 | private ICoapEndpoint _endpoint; 15 | private Uri _address = null; 16 | 17 | [Key] 18 | public int Id { get; set; } 19 | 20 | [NotMapped] 21 | public ICoapEndpoint Endpoint 22 | { 23 | get => _endpoint ?? (_endpoint = CoapEndpointFactory.GetEndpoint(Address)); 24 | set => _endpoint = value; 25 | } 26 | 27 | public ICollection KnownResources { get; set; } 28 | = new ObservableCollection(); 29 | 30 | public EndpointType EndpointType { get; set; } 31 | 32 | public bool IsFavourite { get; set; } 33 | 34 | public string Name { get; set; } = string.Empty; 35 | 36 | [NotMapped] 37 | public Uri Address { get => _address ?? (_address = Endpoint.BaseUri); set => _address = value; } 38 | public DateTime LastSeen { get; set; } = DateTime.MinValue; 39 | 40 | [Column(nameof(Address))] 41 | public string _dbAddress 42 | { 43 | get => Address?.ToString(); 44 | set 45 | { 46 | if (value == null) 47 | { 48 | Address = null; 49 | return; 50 | } 51 | 52 | Address = new Uri(value, UriKind.Absolute); 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /src/CoAPExplorer/Models/DeviceResource.cs: -------------------------------------------------------------------------------- 1 | using CoAPNet.Options; 2 | using System; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.ComponentModel.DataAnnotations.Schema; 5 | 6 | namespace CoAPExplorer.Models 7 | { 8 | public class DeviceResource 9 | { 10 | [Key] 11 | public int Id { get; set; } 12 | 13 | public Device Device { get; set; } 14 | 15 | [NotMapped] 16 | public Uri Url { get; set; } 17 | 18 | public string Name { get; set; } 19 | 20 | [NotMapped] 21 | public ContentFormatType ContentFormat { get; set; } = null; 22 | 23 | [Column(nameof(Url))] 24 | public string _dbUrl 25 | { 26 | get => Url?.ToString(); 27 | set 28 | { 29 | if (value == null) 30 | { 31 | Url = null; 32 | return; 33 | } 34 | 35 | Url = new Uri(value, UriKind.RelativeOrAbsolute); 36 | } 37 | } 38 | 39 | [Column(nameof(ContentFormat))] 40 | public string _dbContentFormat 41 | { 42 | get => ContentFormat != null ? $"{ContentFormat.Value} - {ContentFormat}" : null; 43 | set 44 | { 45 | if (value == null) 46 | { 47 | ContentFormat = null; 48 | return; 49 | } 50 | 51 | var p = value.Split(new[] { " - " }, StringSplitOptions.RemoveEmptyEntries); 52 | ContentFormat = new ContentFormatType(int.Parse(p[0]), p[1]); 53 | } 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /src/CoAPExplorer/Models/Message.cs: -------------------------------------------------------------------------------- 1 | using CoAPExplorer.Database; 2 | using CoAPExplorer.Extensions; 3 | using CoAPNet; 4 | using CoAPNet.Options; 5 | using ReactiveUI; 6 | using System; 7 | using System.Collections.Generic; 8 | using System.Collections.ObjectModel; 9 | using System.ComponentModel.DataAnnotations; 10 | using System.ComponentModel.DataAnnotations.Schema; 11 | using System.IO; 12 | using System.Linq; 13 | using System.Runtime.Serialization; 14 | using System.Text; 15 | 16 | namespace CoAPExplorer.Models 17 | { 18 | public class Message 19 | { 20 | private byte[] _token; 21 | private byte[] _payload; 22 | private Stream _payloadStream; 23 | 24 | [Key] 25 | public int Id { get; set; } 26 | 27 | [NotMapped] 28 | public int MessageId { get; set; } 29 | 30 | 31 | [NotMapped] 32 | public byte[] Token 33 | { 34 | get => _token; 35 | set 36 | { 37 | if ((value?.Length ?? 0) > 8) 38 | throw new ArgumentException($"{nameof(Token)} may have no moer than 8 bytes"); 39 | _token = value; 40 | } 41 | } 42 | 43 | [NotMapped] 44 | public CoapMessageCode Code { get; set; } = CoapMessageCode.None; 45 | 46 | [NotMapped] 47 | public Uri Url { get; set; } = new Uri(string.Empty, UriKind.RelativeOrAbsolute); 48 | 49 | [NotMapped] 50 | public IList Options { get; set; } = new List(); 51 | 52 | [NotMapped] 53 | public ContentFormatType ContentFormat { get; set; } = null; 54 | 55 | public byte[] Payload 56 | { 57 | get => _payload; 58 | set 59 | { 60 | if (_payloadStream != null) 61 | throw new InvalidOperationException($"Please set {nameof(PayloadStream)} to null before assigning to {nameof(Payload)}"); 62 | _payload = value; 63 | } 64 | } 65 | 66 | [NotMapped] 67 | public Stream PayloadStream 68 | { 69 | get => _payloadStream; 70 | set 71 | { 72 | if (_payload != null) 73 | throw new InvalidOperationException($"Please set {nameof(Payload)} to null before assigning to {nameof(PayloadStream)}"); 74 | _payloadStream = value; 75 | } 76 | } 77 | 78 | [Column(nameof(Code))] 79 | public string _dbCode 80 | { 81 | get => Code.ToString() ?? "0.00"; 82 | set 83 | { 84 | var p = value.Split('.'); 85 | Code = new CoapMessageCode(int.Parse(p[0]), int.Parse(p[1])); 86 | } 87 | } 88 | 89 | [Column(nameof(ContentFormat))] 90 | public string _dbContentFormat 91 | { 92 | get => ContentFormat != null ? $"{ContentFormat.Value} - {ContentFormat}" : null; 93 | set 94 | { 95 | if (value == null) 96 | { 97 | ContentFormat = null; 98 | return; 99 | } 100 | 101 | var p = value.Split(new[] { " - " }, StringSplitOptions.RemoveEmptyEntries); 102 | ContentFormat = new ContentFormatType(int.Parse(p[0]), p[1]); 103 | } 104 | } 105 | 106 | [Column(nameof(Url))] 107 | public string _dbUrl 108 | { 109 | get => Url?.ToString(); 110 | set 111 | { 112 | if (value == null) 113 | { 114 | Url = null; 115 | return; 116 | } 117 | 118 | Url = new Uri(value, UriKind.RelativeOrAbsolute); 119 | } 120 | } 121 | 122 | [Column(nameof(Options))] 123 | public string _dbOptions 124 | { 125 | get => Newtonsoft.Json.JsonConvert.SerializeObject(Options, new CoapOptionConverter()); 126 | set 127 | { 128 | var options = Newtonsoft.Json.JsonConvert.DeserializeObject>(value, new CoapOptionConverter()); 129 | Options = new ObservableCollection(options); 130 | } 131 | } 132 | 133 | public override bool Equals(object obj) 134 | { 135 | if (obj is Message message) 136 | { 137 | if (message.Url != Url) 138 | return false; 139 | if (message.Code != Code) 140 | return false; 141 | if (message.Options.Any(o => !Options.Contains(o))) 142 | return false; 143 | if (message.Payload != Payload) 144 | return false; 145 | return true; 146 | } 147 | return base.Equals(obj); 148 | } 149 | 150 | public override int GetHashCode() 151 | { 152 | // Well, there aren't any immutable properties... so this will do. 153 | return 12425; 154 | } 155 | 156 | public override string ToString() 157 | { 158 | return this.ToCoapMessage().ToString(); 159 | } 160 | 161 | public Message Clone() 162 | { 163 | return new Message 164 | { 165 | MessageId = MessageId, 166 | Token = Token?.Clone() as byte[], 167 | Url = Url, 168 | Code = Code, 169 | ContentFormat = ContentFormat, 170 | Options = new List(Options), 171 | Payload = Payload?.Clone() as byte[], 172 | }; 173 | } 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /src/CoAPExplorer/Models/NavigationItem.cs: -------------------------------------------------------------------------------- 1 | using ReactiveUI; 2 | 3 | namespace CoAPExplorer.Models 4 | { 5 | public class NavigationItem 6 | { 7 | public string Name { get; set; } 8 | 9 | public ReactiveCommand Command { get; set; } 10 | 11 | public CoapExplorerIcon Icon { get; set; } 12 | } 13 | } -------------------------------------------------------------------------------- /src/CoAPExplorer/Models/RequestFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace CoAPExplorer.Models 6 | { 7 | public class RequestFilter 8 | { 9 | public string Key { get; set; } 10 | public string Value { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/CoAPExplorer/Models/ToastNotification.cs: -------------------------------------------------------------------------------- 1 | using ReactiveUI; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | 6 | namespace CoAPExplorer.Models 7 | { 8 | public enum ToastNotificationType 9 | { 10 | Debug, 11 | Information, 12 | Warning, 13 | Error, 14 | } 15 | 16 | public readonly struct ToastNotification 17 | { 18 | public readonly string Message; 19 | 20 | public readonly ToastNotificationType Type; 21 | 22 | public readonly IReadOnlyList<(string Label, ReactiveCommand Command)> Actions; 23 | 24 | public ToastNotification(string message, ToastNotificationType type = ToastNotificationType.Information, params (string Label, ReactiveCommand Command)[] actions) 25 | { 26 | Message = message; 27 | 28 | Type = type; 29 | 30 | Actions = actions; 31 | } 32 | 33 | public ToastNotification(string message, ToastNotificationType type = ToastNotificationType.Information, params Tuple[] actions) 34 | { 35 | Message = message; 36 | 37 | Type = type; 38 | 39 | Actions = actions.Select(t => (t.Item1, t.Item2)).ToList(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/CoAPExplorer/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.CompilerServices; 3 | using System.Runtime.InteropServices; 4 | 5 | [assembly:InternalsVisibleTo("Database")] -------------------------------------------------------------------------------- /src/CoAPExplorer/Services/CoapEndpointFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net; 4 | using System.Text; 5 | 6 | using CoAPNet; 7 | using CoAPNet.Udp; 8 | using System.Linq; 9 | 10 | namespace CoAPExplorer.Services 11 | { 12 | 13 | public class CoapEndpointFactory 14 | { 15 | static CoapEndpointFactory() 16 | { 17 | CoapStyleUriParser.Register(); 18 | } 19 | 20 | public static Uri CreateUriFromAddress(string address) 21 | { 22 | if (!address.Contains("://")) 23 | address = "coap://" + address; 24 | 25 | return new Uri(address, UriKind.Absolute); 26 | } 27 | 28 | public static ICoapEndpoint GetEndpoint(Uri address, bool defaultEndpoint = true) 29 | { 30 | if (address == null) 31 | throw new ArgumentNullException(nameof(address)); 32 | 33 | if(address.Scheme == "coap") 34 | { 35 | int port = address.IsDefaultPort && defaultEndpoint ? Coap.Port : 0; 36 | return new CoapUdpEndPoint(ParseIPEndpoint(address.Host, port)); 37 | } 38 | 39 | return new CoapEndpoint(); 40 | } 41 | 42 | public static IPEndPoint ParseIPEndpoint(string endpointstring, int defaultport = -1) 43 | { 44 | if (string.IsNullOrEmpty(endpointstring) 45 | || endpointstring.Trim().Length == 0) 46 | { 47 | throw new ArgumentException("Endpoint descriptor may not be empty."); 48 | } 49 | 50 | if (defaultport != -1 && 51 | (defaultport < IPEndPoint.MinPort 52 | || defaultport > IPEndPoint.MaxPort)) 53 | { 54 | throw new ArgumentException(string.Format("Invalid default port '{0}'", defaultport)); 55 | } 56 | 57 | string[] values = endpointstring.Split(new char[] { ':' }); 58 | IPAddress ipaddy; 59 | int port = -1; 60 | 61 | //check if we have an IPv6 or ports 62 | if (values.Length <= 2) // ipv4 or hostname 63 | { 64 | if (values.Length == 1) 65 | //no port is specified, default 66 | port = defaultport; 67 | else 68 | port = GetPort(values[1]); 69 | 70 | //try to use the address as IPv4, otherwise get hostname 71 | if (!IPAddress.TryParse(values[0], out ipaddy)) 72 | ipaddy = ResolveHost(values[0]); 73 | } 74 | else if (values.Length > 2) //ipv6 75 | { 76 | //could [a:b:c]:d 77 | if (values[0].StartsWith("[") && values[values.Length - 2].EndsWith("]")) 78 | { 79 | string ipaddressstring = string.Join(":", values.Take(values.Length - 1).ToArray()); 80 | ipaddy = IPAddress.Parse(ipaddressstring); 81 | port = GetPort(values[values.Length - 1]); 82 | } 83 | else //[a:b:c] or a:b:c 84 | { 85 | ipaddy = IPAddress.Parse(endpointstring); 86 | port = defaultport; 87 | } 88 | } 89 | else 90 | { 91 | throw new FormatException(string.Format("Invalid endpoint ipaddress '{0}'", endpointstring)); 92 | } 93 | 94 | if (port == -1) 95 | throw new ArgumentException(string.Format("No port specified: '{0}'", endpointstring)); 96 | 97 | return new IPEndPoint(ipaddy, port); 98 | } 99 | 100 | private static int GetPort(string p) 101 | { 102 | int port; 103 | 104 | if (!int.TryParse(p, out port) 105 | || port < IPEndPoint.MinPort 106 | || port > IPEndPoint.MaxPort) 107 | { 108 | throw new FormatException(string.Format("Invalid end point port '{0}'", p)); 109 | } 110 | 111 | return port; 112 | } 113 | 114 | private static IPAddress ResolveHost(string p) 115 | { 116 | var hosts = Dns.GetHostAddresses(p); 117 | 118 | if (hosts == null || hosts.Length == 0) 119 | throw new ArgumentException(string.Format("Host not found: {0}", p)); 120 | 121 | return hosts[0]; 122 | } 123 | } 124 | } -------------------------------------------------------------------------------- /src/CoAPExplorer/Services/CoapService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reactive.Linq; 4 | using System.Text; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | using CoAPNet; 9 | using Splat; 10 | 11 | using CoAPExplorer.Extensions; 12 | using CoAPExplorer.Models; 13 | using System.IO; 14 | 15 | namespace CoAPExplorer.Services 16 | { 17 | public class CoapService 18 | { 19 | private readonly CoapClient _coapClient; 20 | private readonly ICoapEndpoint _endpoint; 21 | 22 | public CoapService(Device targetDevice) 23 | : this(new UriBuilder { Scheme = targetDevice.Address.Scheme, Host = "0.0.0.0", Port = 0 }.Uri) 24 | { } 25 | 26 | public CoapService(string scheme) 27 | :this(new UriBuilder { Scheme = scheme, Host = "0.0.0.0", Port = 0 }.Uri) 28 | { } 29 | 30 | public CoapService(Uri listenAddress) 31 | { 32 | _endpoint = CoapEndpointFactory.GetEndpoint(listenAddress); 33 | _coapClient = new CoapClient(_endpoint); 34 | } 35 | 36 | public IObservable SendMessage(Message message, ICoapEndpoint endpoint) 37 | { 38 | if (message is null) 39 | return Observable.Empty(); 40 | 41 | var coapMessage = message.ToCoapMessage(); 42 | var messageContext = coapMessage.CreateBlockWiseContext(_coapClient); 43 | 44 | return Observable.Create(observer => 45 | { 46 | var cts = new CancellationTokenSource(); 47 | Task.Run(async () => 48 | { 49 | try 50 | { 51 | if (coapMessage.IsBlockWise()) 52 | { 53 | using (var writer = new CoapBlockStreamWriter(messageContext, endpoint)) 54 | await message.PayloadStream.CopyToAsync(writer, writer.BlockSize); 55 | } 56 | else 57 | { 58 | var id = await _coapClient.SendAsync(coapMessage, endpoint, cts.Token); 59 | messageContext = new CoapBlockWiseContext(_coapClient, coapMessage, await _coapClient.GetResponseAsync(id, cts.Token)); 60 | } 61 | 62 | var response = messageContext.Response.ToMessage(); 63 | 64 | if (messageContext.Response.IsBlockWise()) 65 | { 66 | var memoryStream = new MemoryStream(); 67 | 68 | using (var reader = new CoapBlockStreamReader(messageContext, endpoint)) 69 | reader.CopyTo(memoryStream); 70 | 71 | response.Payload = memoryStream.ToArray(); 72 | } 73 | 74 | observer.OnNext(response); 75 | 76 | observer.OnCompleted(); 77 | } 78 | catch(Exception ex) 79 | { 80 | observer.OnError(ex); 81 | } 82 | }); 83 | 84 | return cts.Cancel; 85 | }); 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/CoAPExplorer/Services/IDiscoveryService.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using CoAPExplorer.Models; 4 | 5 | namespace CoAPExplorer.Services 6 | { 7 | public interface IDiscoveryService 8 | { 9 | IObservable DiscoverDevices(); 10 | IObservable DiscoverResources(Device device); 11 | void SetFilters(IEnumerable filters); 12 | void SetRequstUrl(string Url); 13 | void SetTimeout(TimeSpan timeout); 14 | } 15 | } -------------------------------------------------------------------------------- /src/CoAPExplorer/Services/MyDebugLogger.cs: -------------------------------------------------------------------------------- 1 | #define DEBUG 2 | 3 | using Splat; 4 | using System.ComponentModel; 5 | 6 | namespace CoAPExplorer.Services 7 | { 8 | public class MyDebugLogger : ILogger 9 | { 10 | public LogLevel Level { get; set; } 11 | 12 | public void Write([Localizable(false)] string message, LogLevel logLevel) 13 | { 14 | if ((int)logLevel < (int)Level) return; 15 | System.Diagnostics.Debug.WriteLine(message); 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /src/CoAPExplorer/Utils/CoapPayloadFormatConverter.cs: -------------------------------------------------------------------------------- 1 | using CoAPExplorer.Models; 2 | using CoAPNet.Options; 3 | using Newtonsoft.Json; 4 | using Newtonsoft.Json.Linq; 5 | using System; 6 | using System.Collections.Generic; 7 | using System.Text; 8 | 9 | namespace CoAPExplorer.Utils 10 | { 11 | public class CoapPayloadFormater 12 | { 13 | public static string Format(byte[] payload, ContentFormatType contentFormat) 14 | { 15 | if (payload == null || contentFormat == null) 16 | return string.Empty; 17 | 18 | if(payload.Length == 0) 19 | return string.Empty; 20 | 21 | try 22 | { 23 | if (contentFormat.Value == ContentFormatType.TextPlain.Value) 24 | return Encoding.UTF8.GetString(payload); 25 | 26 | if (contentFormat.Value == ContentFormatType.ApplicationJson.Value) 27 | return JToken.Parse(Encoding.UTF8.GetString(payload)).ToString(Newtonsoft.Json.Formatting.Indented); 28 | 29 | // TODO: Don't simply replace all commas with new line. Need to be context aware to ensure we're not splitting a link format in two. 30 | if (contentFormat.Value == ContentFormatType.ApplicationLinkFormat.Value) 31 | return Encoding.UTF8.GetString(payload).Replace(",", ",\r\n"); 32 | } 33 | catch(Exception ex) 34 | { 35 | System.Diagnostics.Debug.WriteLine(ex); 36 | #if DEBUG 37 | return ex.ToString(); 38 | #endif 39 | } 40 | 41 | return string.Empty; 42 | } 43 | 44 | public static byte[] RemoveFormat(string payload, ContentFormatType contentFormat) 45 | { 46 | if (string.IsNullOrEmpty(payload) || contentFormat == null) 47 | return null; 48 | 49 | try 50 | { 51 | 52 | if (contentFormat.Value == ContentFormatType.TextPlain.Value) 53 | return Encoding.UTF8.GetBytes(payload); 54 | 55 | if (contentFormat.Value == ContentFormatType.ApplicationJson.Value) 56 | return Encoding.UTF8.GetBytes(JToken.Parse(payload).ToString()); 57 | 58 | if (contentFormat.Value == ContentFormatType.ApplicationLinkFormat.Value) 59 | return Encoding.UTF8.GetBytes(payload.Replace("\n,",",").Replace("\r","")); 60 | } 61 | catch (JsonReaderException ex) 62 | { 63 | throw new FormattedTextException(ex.Message, ex.LineNumber, ex.LinePosition, ex); 64 | } 65 | 66 | return null; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/CoAPExplorer/Utils/StringEscape.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Text.RegularExpressions; 7 | using System.Threading.Tasks; 8 | 9 | namespace CoAPExplorer.Utils 10 | { 11 | public static class StringEscape 12 | { 13 | public static string Escape(string value) 14 | { 15 | Func escapeChar = c => 16 | { 17 | switch (c) 18 | { 19 | case '\\': return @"\\"; 20 | case '\0': return @"\0"; 21 | case '\a': return @"\a"; 22 | case '\b': return @"\b"; 23 | case '\f': return @"\f"; 24 | case '\n': return @"\n"; 25 | case '\r': return @"\r"; 26 | case '\t': return @"\t"; 27 | case '\v': return @"\v"; 28 | } 29 | if ((int)c <= 255) 30 | return "\\x" + ((int)c).ToString("x2"); 31 | return "\\u" + ((int)c).ToString("x4"); 32 | }; 33 | 34 | StringBuilder sb = new StringBuilder(); 35 | for (int i = 0; i < value.Length; i++) 36 | { 37 | var c = value[i]; 38 | char[] controlChars = new[] { '\\', '0', 'a', 'b', 'f', 'l', 'n', 'r', 't', 'v', 'x', 'u', 'U' }; 39 | 40 | if (char.IsControl(c) && !char.IsWhiteSpace(c)) 41 | sb.Append(escapeChar(c)); 42 | else if (c == '\\' && controlChars.Contains(value[i + 1])) 43 | sb.Append(@"\\"); 44 | else if (char.IsSurrogatePair(value, i)) 45 | sb.Append("\\U" + char.ConvertToUtf32(value, i++).ToString("x8")); 46 | else if (c > 127) 47 | sb.Append("\\u" + ((int)c).ToString("x4")); 48 | else 49 | sb.Append(c); 50 | } 51 | 52 | return sb.ToString(); 53 | } 54 | 55 | public static string Unescape(string value) 56 | { 57 | return Regex.Replace(value, @"\\(?:(\\|0|a|b|f|n|r|t|v)|U([a-fA-F0-9]{8})|u([a-fA-F0-9]{4})|x([a-fA-F0-9]{1,4}))", 58 | matches => { 59 | string match = matches.Groups[1].Success ? matches.Groups[1].Value // control characters 60 | : matches.Groups[2].Success ? matches.Groups[2].Value // 8-digit unicode sequence 61 | : matches.Groups[3].Success ? matches.Groups[3].Value // 4-digit unicdoe sequence 62 | : matches.Groups[4].Success ? matches.Groups[4].Value // 1 to 4-digit hex sequence 63 | : throw new NotImplementedException($"Regex issue in {nameof(StringEscape)}"); 64 | 65 | if (matches.Groups[1].Success) 66 | { 67 | switch (match) 68 | { 69 | case @"\": return "\\"; 70 | case @"0": return "\0"; 71 | case @"a": return "\a"; 72 | case @"b": return "\b"; 73 | case @"f": return "\f"; 74 | case @"n": return "\n"; 75 | case @"r": return "\r"; 76 | case @"t": return "\t"; 77 | case @"v": return "\v"; 78 | default: 79 | throw new NotImplementedException($"Unsupported escape character ({match})"); 80 | } 81 | } 82 | return (char.ConvertFromUtf32(int.Parse(match, NumberStyles.HexNumber))); 83 | }); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/CoAPExplorer/ViewModels/DeviceNavigationViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel; 4 | using System.Linq; 5 | using System.Reactive.Disposables; 6 | using System.Reactive.Linq; 7 | using System.Text; 8 | 9 | using ReactiveUI; 10 | using Splat; 11 | 12 | using CoAPExplorer.Database; 13 | using CoAPExplorer.Models; 14 | using CoAPExplorer.Services; 15 | 16 | namespace CoAPExplorer.ViewModels 17 | { 18 | public class DeviceNavigationViewModel : ReactiveObject, ISupportsActivation 19 | { 20 | private bool _isOpen = true; 21 | private IDiscoveryService _discoveryService; 22 | private readonly CoapExplorerContext _context; 23 | private bool _pendingClearResources = false; 24 | private DeviceViewModel _device; 25 | public DeviceResource _selectedResource; 26 | 27 | public bool IsOpen { get => _isOpen; set => this.RaiseAndSetIfChanged(ref _isOpen, value); } 28 | 29 | public Device Device => _device.Device; 30 | 31 | public IReactiveDerivedList Resources => 32 | _device.Resources.CreateDerivedCollection(x => x, orderer: (a, b) => Uri.Compare(a.Url, b.Url, UriComponents.Path, UriFormat.Unescaped, StringComparison.InvariantCultureIgnoreCase)); 33 | 34 | public DeviceResource SelectedResource { get => _selectedResource; set => this.RaiseAndSetIfChanged(ref _selectedResource, value); } 35 | 36 | public string Name => _device.Name; 37 | 38 | public string Address => _device.Address; 39 | 40 | public ViewModelActivator Activator { get; } = new ViewModelActivator(); 41 | 42 | public ReactiveCommand RefreshResourcesCommand; 43 | 44 | public DeviceNavigationViewModel(DeviceViewModel device) 45 | { 46 | _device = device; 47 | _discoveryService = Locator.Current.GetService(); 48 | _context = Locator.Current.GetService(); 49 | 50 | _device.PropertyChanged += DevicePropertyChanged; 51 | 52 | RefreshResourcesCommand = ReactiveCommand.CreateFromObservable(d => 53 | { 54 | _pendingClearResources = true; 55 | return _discoveryService.DiscoverResources(d).Do(_ => { }, async () => await _context.SaveChangesAsync()); 56 | }); 57 | 58 | RefreshResourcesCommand.Subscribe(resource => 59 | { 60 | if (_pendingClearResources) 61 | _device.Device.KnownResources.Clear(); 62 | 63 | _pendingClearResources = false; 64 | _device.Device.KnownResources.Add(resource); 65 | }); 66 | 67 | this.WhenActivated(disposables => 68 | { 69 | RefreshResourcesCommand 70 | .ThrownExceptions 71 | .Subscribe(ex => App.LogException(ex)) 72 | .DisposeWith(disposables); 73 | }); 74 | } 75 | 76 | private void DevicePropertyChanged(object sender, PropertyChangedEventArgs e) 77 | { 78 | switch (e.PropertyName) 79 | { 80 | case nameof(_device.Name): 81 | this.RaisePropertyChanged(nameof(Name)); 82 | break; 83 | case nameof(_device.Address): 84 | this.RaisePropertyChanged(nameof(Address)); 85 | break; 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/CoAPExplorer/ViewModels/HomeViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Text; 3 | 4 | using ReactiveUI; 5 | using Splat; 6 | 7 | namespace CoAPExplorer.ViewModels 8 | { 9 | public class HomeViewModel : ReactiveObject, IRoutableViewModel 10 | { 11 | private SearchViewModel _search; 12 | private RecentDevicesViewModel _recentDevices; 13 | 14 | public RecentDevicesViewModel RecentDevices 15 | { 16 | get => _recentDevices ?? (_recentDevices = new RecentDevicesViewModel(HostScreen)); 17 | set => _recentDevices = value; 18 | } 19 | 20 | public SearchViewModel Search 21 | { 22 | get => _search ?? (_search = new SearchViewModel(HostScreen)); 23 | set => _search = value; 24 | } 25 | 26 | public IScreen HostScreen { get; } 27 | 28 | public string UrlPathSegment => ""; 29 | 30 | public HomeViewModel(IScreen screen = null) 31 | { 32 | HostScreen = screen ?? Locator.Current.GetService(); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/CoAPExplorer/ViewModels/NavigationViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Reactive; 4 | using System.Reactive.Linq; 5 | using System.Text; 6 | using CoAPExplorer.Models; 7 | using ReactiveUI; 8 | 9 | namespace CoAPExplorer.ViewModels 10 | { 11 | public class NavigationViewModel : ReactiveObject 12 | { 13 | private bool _isOpen = false; 14 | private NavigationItem _selectedNavigationItem; 15 | 16 | public bool IsOpen { get => _isOpen; set => this.RaiseAndSetIfChanged(ref _isOpen, value); } 17 | 18 | public ReactiveList NavigationItems { get; set; } 19 | 20 | public NavigationItem SelectedNavigationItem 21 | { 22 | get => _selectedNavigationItem; 23 | set 24 | { 25 | //if (_selectedNavigationItem == value) 26 | // return; 27 | _selectedNavigationItem = null; 28 | 29 | if(value?.Command != null) 30 | Observable.Return(Unit.Default).InvokeCommand(value.Command); 31 | 32 | this.RaisePropertyChanged(nameof(SelectedNavigationItem)); 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/CoAPExplorer/ViewModels/NewDeviceViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reactive; 5 | using System.Text; 6 | 7 | using ReactiveUI; 8 | using Splat; 9 | 10 | using CoAPExplorer.Models; 11 | using CoAPExplorer.Services; 12 | using CoAPExplorer.Extensions; 13 | using CoAPExplorer.Database; 14 | 15 | namespace CoAPExplorer.ViewModels 16 | { 17 | public class NewDeviceViewModel : ReactiveObject 18 | { 19 | private string _name; 20 | private string _address; 21 | private readonly CoapExplorerContext _dbContext; 22 | private readonly IScreen _screen; 23 | 24 | public string Name { get => _name; set => this.RaiseAndSetIfChanged(ref _name, value); } 25 | 26 | public string Address { get => _address; set => this.RaiseAndSetIfChanged(ref _address, value); } 27 | 28 | public ReactiveCommand AddDeviceCommand { get; } 29 | 30 | public NewDeviceViewModel(IScreen screen = null) 31 | { 32 | _dbContext = Locator.Current.GetService(); 33 | 34 | _screen = screen ?? Locator.Current.GetService(); 35 | 36 | AddDeviceCommand = ReactiveCommand.CreateFromTask(async () => 37 | { 38 | // TODO: User Input Validation 39 | // TODO: Can re add another devie with the same Endpoint address? 40 | var address = CoapEndpointFactory.CreateUriFromAddress(Address); 41 | var device = new Device 42 | { 43 | Name = Name, 44 | Address = address, 45 | Endpoint = CoapEndpointFactory.GetEndpoint(address), 46 | }; 47 | 48 | _screen.Router.Navigate.Execute(new DeviceViewModel(device, _screen)) 49 | .Subscribe(); 50 | 51 | if (_dbContext != null) 52 | { 53 | await _dbContext.Devices.AddAsync(device); 54 | await _dbContext.SaveChangesAsync(); 55 | } 56 | }); 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/CoAPExplorer/ViewModels/RecentDevicesViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Collections.ObjectModel; 4 | using System.Linq; 5 | using System.Reactive; 6 | using System.Reactive.Disposables; 7 | using System.Reactive.Linq; 8 | using System.Text; 9 | 10 | using Microsoft.EntityFrameworkCore; 11 | using Splat; 12 | using ReactiveUI; 13 | 14 | using CoAPExplorer.Database; 15 | using CoAPExplorer.Models; 16 | using CoAPExplorer.Services; 17 | 18 | namespace CoAPExplorer.ViewModels 19 | { 20 | public class RecentDevicesViewModel : ReactiveObject, ISupportsActivation 21 | { 22 | private readonly CoapExplorerContext _dbContext; 23 | private readonly IScreen _screen; 24 | private ObservableAsPropertyHelper _isSearchValidUri = ObservableAsPropertyHelper.Default(); 25 | private string _searchTerms; 26 | private ObservableCollection _devices; 27 | private IReactiveDerivedList _filteredDevices; 28 | 29 | public ReactiveCommand OpenDeviceCommand { get; } 30 | 31 | public ReactiveCommand RemoveDeviceCommand { get; } 32 | 33 | public ReactiveCommand NavigateToUriCommand { get; } 34 | 35 | public string SearchTerms { get => _searchTerms; set => this.RaiseAndSetIfChanged(ref _searchTerms, value); } 36 | 37 | public bool IsSearchValidUri => _isSearchValidUri.Value; 38 | 39 | public ReactiveCommand AddDeviceCommand { get; } 40 | 41 | public ObservableCollection Devices 42 | { 43 | get => _devices ?? (_devices = new ObservableCollection(_dbContext.Devices.Include(x => x.KnownResources).Select(d => new DeviceViewModel(d, _screen)))); 44 | set => _devices = value; 45 | } 46 | 47 | public IReactiveDerivedList FilteredDevices => _filteredDevices ?? (_filteredDevices = Devices.CreateDerivedCollection(x => x, FilterDevicesBySearhTerms, signalReset: this.WhenAnyValue(vm => vm.SearchTerms))); 48 | 49 | public ViewModelActivator Activator { get; } = new ViewModelActivator(); 50 | 51 | public RecentDevicesViewModel(IScreen screen = null, CoapExplorerContext context = null) 52 | { 53 | _screen = screen ?? Locator.Current.GetService(); 54 | _dbContext = context ?? Locator.Current.GetService(); 55 | 56 | AddDeviceCommand = ReactiveCommand.Create(_ => 57 | { 58 | System.Diagnostics.Debug.WriteLine("Creating new device dialog thingy"); 59 | return new NewDeviceViewModel(screen); 60 | }); 61 | 62 | RemoveDeviceCommand = ReactiveCommand.CreateFromTask(async device => 63 | { 64 | _dbContext.Devices.Remove(device); 65 | _devices.Remove(_devices.Single(dvm => dvm.Device.Id == device.Id)); 66 | await _dbContext.SaveChangesAsync(); 67 | }); 68 | 69 | OpenDeviceCommand = ReactiveCommand.Create(device => Observable.Return(Unit.Default).InvokeCommand(device.OpenCommand)); 70 | 71 | NavigateToUriCommand = ReactiveCommand.CreateFromObservable(() => 72 | { 73 | if (!Uri.TryCreate(SearchTerms, UriKind.Absolute, out var uri)) 74 | return Observable.Empty(); 75 | 76 | var deviceViewModel = new DeviceViewModel(new Device { Address = uri }, screen); 77 | 78 | return screen.Router.Navigate.Execute(deviceViewModel); 79 | }, this.WhenAnyValue(vm => vm.IsSearchValidUri)); 80 | 81 | this.WhenActivated(disposables => 82 | { 83 | _isSearchValidUri = this.WhenAnyValue(vm => vm.SearchTerms) 84 | .Select(x => Uri.TryCreate(x, UriKind.Absolute, out _)) 85 | .ToProperty(this, vm => vm.IsSearchValidUri) 86 | .DisposeWith(disposables); 87 | }); 88 | } 89 | 90 | private bool FilterDevicesBySearhTerms(DeviceViewModel device) 91 | { 92 | if (IsSearchValidUri) 93 | return false; 94 | 95 | if (string.IsNullOrEmpty(SearchTerms)) 96 | return true; 97 | 98 | var search = SearchTerms.ToLowerInvariant(); 99 | 100 | if (device.Name.ToLowerInvariant().Contains(search) || 101 | device.Address.ToLowerInvariant().Contains(search)) 102 | return true; 103 | 104 | // TODO: Add more places to search (i.e. hostname, history, resources) 105 | 106 | return false; 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/CoAPExplorer/ViewModels/SearchViewModel.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Collections.Generic; 4 | using System.Collections.ObjectModel; 5 | using System.Reactive; 6 | using System.Reactive.Linq; 7 | using System.Reactive.Disposables; 8 | using System.Threading.Tasks; 9 | 10 | using CoAPNet; 11 | using ReactiveUI; 12 | using Splat; 13 | 14 | using CoAPExplorer.Models; 15 | using CoAPExplorer.Services; 16 | 17 | namespace CoAPExplorer.ViewModels 18 | { 19 | public class SearchViewModel : ReactiveObject, ISupportsActivation 20 | { 21 | private string _searchUrl = "/.well-known/core"; 22 | private ObservableAsPropertyHelper _isSearching; 23 | private readonly IDiscoveryService _discoveryService; 24 | private ReactiveList _devices; 25 | 26 | public IScreen Router { get; } 27 | 28 | public List Filters { get; set; } 29 | 30 | public ReactiveCommand AddFilter { get; } 31 | 32 | public ReactiveCommand RemoveFilter { get; } 33 | 34 | public ReactiveCommand SearchCommand { get; } 35 | 36 | public ReactiveCommand StopCommand { get; } 37 | 38 | public ReactiveCommand OpenDeviceCommand { get; } 39 | 40 | public bool IsSearching => _isSearching.Value; 41 | 42 | public ViewModelActivator Activator { get; } = new ViewModelActivator(); 43 | 44 | public ReactiveList Devices 45 | { 46 | get => _devices ?? (_devices = new ReactiveList()); 47 | set => _devices = value; 48 | } 49 | 50 | public string SearchUrl 51 | { 52 | get { return _searchUrl; } 53 | set { this.RaiseAndSetIfChanged(ref _searchUrl, value); } 54 | } 55 | 56 | public SearchViewModel(IScreen router = null) 57 | { 58 | Router = router ?? Locator.Current.GetService(); 59 | 60 | _discoveryService = Locator.Current.GetService(); 61 | 62 | Filters = new List(); 63 | 64 | AddFilter = ReactiveCommand.Create(() => Filters.Add(new RequestFilter())); 65 | RemoveFilter = ReactiveCommand.Create(f => Filters.Remove(f)); 66 | 67 | SearchCommand = ReactiveCommand 68 | .CreateFromObservable( 69 | () => SearchDevices().TakeUntil(StopCommand)); 70 | 71 | StopCommand = ReactiveCommand.Create( 72 | () => { }, 73 | SearchCommand.IsExecuting); 74 | 75 | //OpenDeviceCommand = ReactiveCommand.CreateFromObservable(device => device.OpenCommand); 76 | OpenDeviceCommand = ReactiveCommand.Create(device => System.Diagnostics.Debug.WriteLine($"we got a device {device.Name}")); 77 | 78 | 79 | this.WhenActivated(disposables => 80 | { 81 | _isSearching = SearchCommand.IsExecuting 82 | .Select(x => x) 83 | .ToProperty(this, x => x.IsSearching, false) 84 | .DisposeWith(disposables); 85 | 86 | SearchCommand.Select(d => new DeviceViewModel(d, router)).Subscribe(device => Devices.Add(device)) 87 | .DisposeWith(disposables); 88 | 89 | // Catch and handle all exceptions produced by observables. 90 | Observable.Merge(StopCommand.ThrownExceptions, SearchCommand.ThrownExceptions, 91 | AddFilter.ThrownExceptions, RemoveFilter.ThrownExceptions) 92 | .Subscribe(ex => App.LogException(ex)) 93 | .DisposeWith(disposables); 94 | }); 95 | } 96 | 97 | public IObservable SearchDevices() 98 | { 99 | _discoveryService.SetRequstUrl(SearchUrl); 100 | _discoveryService.SetFilters(Filters); 101 | return _discoveryService.DiscoverDevices(); 102 | } 103 | } 104 | } --------------------------------------------------------------------------------