diff --git a/.gitignore b/.gitignore index aa202a2..f6a27f1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,34 +1,243 @@ -/PebbleSharp.Core/bin -/PebbleSharp.Core/obj -/PebbleSharp.Core.Tests/bin -/PebbleSharp.Core.Tests/obj -/PebbleSharp.Net45/bin -/PebbleSharp.Net45/obj -/PebbleSharp.Net45.Tests/bin/Debug -/PebbleSharp.WinRT/bin/Debug -/PebbleSharp.WinRT/obj/Debug -/PebbleSharp.Net45.Tests/obj/Debug -/PebbleSharp.Mobile/PebbleSharp.Mobile.WinPhone -/PebbleSharp.Mobile/PebbleSharp.Mobile.iOS -/PebbleSharp.Mobile/PebbleSharp.Mobile.Android/obj/Debug -/Demo/PebbleSharp.WPF -/PebbleSharp.Driod/obj/Debug -/PebbleSharp.iOS/obj/Debug -/PebbleSharp.Mobile/PebbleSharp.Mobile -/PebbleSharp.sln.DotSettings.user -/PebbleSharp.v12.suo -/PebbleSharp.Universal/PebbleSharp.Universal.Windows/bin -/PebbleSharp.Universal/PebbleSharp.Universal.Windows/obj -/PebbleSharp.Universal/PebbleSharp.Universal.WindowsPhone/bin -/PebbleSharp.Universal/PebbleSharp.Universal.WindowsPhone/obj -/PebbleSharp.WinRT.Tests/bin -/PebbleSharp.WinRT.Tests/obj -/PebbleSharp.Universal/PebbleSharp.Universal.WindowsPhone/PebbleSharp.Universal.WindowsPhone.csproj.user -/Demo/PebbleSharp.Universal/PebbleSharp.Universal.Windows/bin -/Demo/PebbleSharp.Universal/PebbleSharp.Universal.Windows/obj -/Demo/PebbleSharp.Universal/PebbleSharp.Universal.WindowsPhone/bin -/Demo/PebbleSharp.Universal/PebbleSharp.Universal.WindowsPhone/obj -/Demo/PebbleSharp.Universal/PebbleSharp.Universal.WindowsPhone/PebbleSharp.Universal.WindowsPhone.csproj.user -/PebbleCmd/bin/Debug -/PebbleCmd/obj/Debug -/PebbleSharp.Core.Tests/PebbleSharp.Core.Tests.csproj.user +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. + +# User-specific files +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ + +# Visual Studio 2015 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUNIT +*.VisualState.xml +TestResult.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# DNX +project.lock.json +artifacts/ + +*_i.c +*_p.c +*_i.h +*.ilk +*.meta +*.obj +*.pch +*.pdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# JustCode is a .NET coding add-in +.JustCode + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# TODO: Comment the next line if you want to checkin your web deploy settings +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# NuGet Packages +*.nupkg +# The packages folder can be ignored because of Package Restore +**/packages/* +# except build/, which is used as an MSBuild target. +!**/packages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/packages/repositories.config +# NuGet v3's project.json files produces more ignoreable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Microsoft Azure ApplicationInsights config file +ApplicationInsights.config + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.pfx +*.publishsettings +node_modules/ +orleans.codegen.cs + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm + +# SQL Server files +*.mdf +*.ldf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe + +# FAKE - F# Make +.fake/ + +# JetBrains Rider +.idea/ +*.sln.imlobj \ No newline at end of file diff --git a/Demo/PebbleSharp.Universal/PebbleSharp.Universal.Shared/ViewModels/AppsViewModel.cs b/Demo/PebbleSharp.Universal/PebbleSharp.Universal.Shared/ViewModels/AppsViewModel.cs index 84b45c7..df283f7 100644 --- a/Demo/PebbleSharp.Universal/PebbleSharp.Universal.Shared/ViewModels/AppsViewModel.cs +++ b/Demo/PebbleSharp.Universal/PebbleSharp.Universal.Shared/ViewModels/AppsViewModel.cs @@ -29,7 +29,7 @@ public override async Task RefreshAsync() private async Task LoadAppsAsync() { - var appBankContents = await Pebble.GetAppbankContentsAsync(); + var appBankContents = await Pebble.InstallClient.GetAppbankContentsAsync(); Apps.Clear(); if (appBankContents != null && appBankContents.AppBank != null && appBankContents.AppBank.Apps != null) { diff --git a/Demo/PebbleSharp.Universal/PebbleSharp.Universal.Windows/PebbleSharp.Universal.Windows.csproj b/Demo/PebbleSharp.Universal/PebbleSharp.Universal.Windows/PebbleSharp.Universal.Windows.csproj index 516090b..c99cd29 100644 --- a/Demo/PebbleSharp.Universal/PebbleSharp.Universal.Windows/PebbleSharp.Universal.Windows.csproj +++ b/Demo/PebbleSharp.Universal/PebbleSharp.Universal.Windows/PebbleSharp.Universal.Windows.csproj @@ -104,11 +104,11 @@ - + {ae0f69ff-97c2-4e55-a3c9-b73e68668dda} PebbleSharp.Core - + {c6f33ca9-eed4-4553-ae74-cd77e6bf3d75} PebbleSharp.WinRT diff --git a/Demo/PebbleSharp.WPF/Messages/PebbleDisconnected.cs b/Demo/PebbleSharp.WPF/Messages/PebbleDisconnected.cs new file mode 100644 index 0000000..269673f --- /dev/null +++ b/Demo/PebbleSharp.WPF/Messages/PebbleDisconnected.cs @@ -0,0 +1,21 @@ +using System; +using PebbleSharp.Core; + +namespace PebbleSharp.WPF.Messages +{ + public class PebbleDisconnected + { + private readonly Pebble _pebble; + + public PebbleDisconnected( Pebble pebble ) + { + if (pebble == null) throw new ArgumentNullException("pebble"); + _pebble = pebble; + } + + public Pebble Pebble + { + get { return _pebble; } + } + } +} \ No newline at end of file diff --git a/Demo/PebbleSharp.WPF/PebbleSharp.WPF.csproj b/Demo/PebbleSharp.WPF/PebbleSharp.WPF.csproj index 0a85a13..fb80187 100644 --- a/Demo/PebbleSharp.WPF/PebbleSharp.WPF.csproj +++ b/Demo/PebbleSharp.WPF/PebbleSharp.WPF.csproj @@ -70,6 +70,7 @@ App.xaml Code + @@ -100,7 +101,6 @@ - Code diff --git a/Demo/PebbleSharp.WPF/ViewModels/PebbleAppsViewModel.cs b/Demo/PebbleSharp.WPF/ViewModels/PebbleAppsViewModel.cs index 953300e..7349898 100644 --- a/Demo/PebbleSharp.WPF/ViewModels/PebbleAppsViewModel.cs +++ b/Demo/PebbleSharp.WPF/ViewModels/PebbleAppsViewModel.cs @@ -4,6 +4,7 @@ using System.Windows.Input; using GalaSoft.MvvmLight.Command; using Microsoft.Win32; +using PebbleSharp.Core; using PebbleSharp.Core.Bundles; using PebbleSharp.WPF.Messages; using PebbleSharp.Net45; @@ -58,7 +59,7 @@ private async Task LoadAppsAsync() return; Loading = true; - var appBankContents = await _pebble.GetAppbankContentsAsync(); + var appBankContents = await _pebble.InstallClient.GetAppbankContentsAsync(); _apps.Clear(); if ( appBankContents.Success ) foreach ( var app in appBankContents.AppBank.Apps ) @@ -91,12 +92,13 @@ private async void OnInstallApp() var bundle = new AppBundle(); using (var zip = new Zip()) { - bundle.Load(openDialog.OpenFile(), zip); + zip.Open(openDialog.OpenFile()); + bundle.Load(zip,_pebble.Firmware.HardwarePlatform.GetSoftwarePlatform()); } if ( _pebble.IsAlive == false ) return; - await _pebble.InstallAppAsync( bundle ); + await _pebble.InstallClient.InstallAppAsync( bundle ); await LoadAppsAsync(); } } diff --git a/Demo/PebbleSharp.WPF/ViewModels/PebbleInfoViewModel.cs b/Demo/PebbleSharp.WPF/ViewModels/PebbleInfoViewModel.cs index a9002b6..101f45c 100644 --- a/Demo/PebbleSharp.WPF/ViewModels/PebbleInfoViewModel.cs +++ b/Demo/PebbleSharp.WPF/ViewModels/PebbleInfoViewModel.cs @@ -101,12 +101,13 @@ private async void OnUpdateFirmware() var bundle = new FirmwareBundle(); using (var zip = new Zip()) { - bundle.Load(openDialog.OpenFile(), zip); + zip.Open(openDialog.OpenFile()); + bundle.Load(zip,_pebble.Firmware.HardwarePlatform.GetSoftwarePlatform()); } if (_pebble.IsAlive == false) return; - await _pebble.InstallFirmwareAsync(bundle); + await _pebble.InstallClient.InstallFirmwareAsync(bundle); } } diff --git a/PebbleCmd/AppMessageTest.pbw b/PebbleCmd/AppMessageTest.pbw new file mode 100644 index 0000000..8b268c2 Binary files /dev/null and b/PebbleCmd/AppMessageTest.pbw differ diff --git a/PebbleCmd/PebbleCmd.csproj b/PebbleCmd/PebbleCmd.csproj index 69615f2..f397065 100644 --- a/PebbleCmd/PebbleCmd.csproj +++ b/PebbleCmd/PebbleCmd.csproj @@ -50,6 +50,9 @@ + + PreserveNewest + diff --git a/PebbleCmd/Program.cs b/PebbleCmd/Program.cs index c970c02..4beaa3c 100644 --- a/PebbleCmd/Program.cs +++ b/PebbleCmd/Program.cs @@ -4,7 +4,10 @@ using System.Threading.Tasks; using PebbleSharp.Core; using PebbleSharp.Core.Responses; +using System.IO; using PebbleSharp.Net45; +using PebbleSharp.Core.Bundles; +using PebbleSharp.Core.AppMessage; namespace PebbleCmd { @@ -29,6 +32,7 @@ private static async Task ShowPebbles() if ( result >= 0 && result < pebbles.Count ) { var selectedPebble = pebbles[result]; + selectedPebble.RegisterCallback(ReceiveAppMessage); Console.WriteLine( "Connecting to Pebble " + selectedPebble.PebbleID ); await selectedPebble.ConnectAsync(); Console.WriteLine( "Connected" ); @@ -44,7 +48,9 @@ private static async Task ShowPebbleMenu( Pebble pebble ) "Set Current Time", "Get Firmware Info", "Send Ping", - "Media Commands" ); + "Media Commands", + "Install App", + "Send App Message"); while ( true ) { switch ( menu.ShowMenu() ) @@ -72,8 +78,127 @@ private static async Task ShowPebbleMenu( Pebble pebble ) case 5: ShowMediaCommands( pebble ); break; + case 6: + InstallApp(pebble); + break; + case 7: + SendAppMessage(pebble); + break; + } + } + } + + private static string SelectApp() + { + string exePath = System.Reflection.Assembly.GetExecutingAssembly ().CodeBase; + //TODO: there has to be a better way to come up with a canonical path, but this combo seems to work on both windows and 'nix + if (exePath.StartsWith("file:")) + { + exePath = exePath.Substring(5); + } + if (exePath.StartsWith("///")) + { + exePath = exePath.Substring(3); + } + string exeDir = Path.GetDirectoryName (exePath); + var dir = new DirectoryInfo (exeDir); + var files = dir.GetFiles ("*.pbw"); + + if (files.Any()) + { + if (files.Count() == 1) + { + return files.Single().FullName; + } + else + { + var fileMenu = new Menu(files.Select(x => x.Name).ToArray()); + int index = fileMenu.ShowMenu(); + return files[index].FullName; + } + } + else + { + Console.WriteLine("No .pbw files found"); + return null; + } + } + + private static void InstallApp(Pebble pebble) + { + var progress = new Progress(pv => Console.WriteLine(pv.ProgressPercentage + " " + pv.Message)); + + string appPath = SelectApp(); + + if (!string.IsNullOrEmpty(appPath) && File.Exists(appPath)) + { + using (var stream = new FileStream(appPath, FileMode.Open)) + { + using (var zip = new Zip()) + { + zip.Open(stream); + var bundle = new AppBundle(); + stream.Position = 0; + bundle.Load(zip,pebble.Firmware.HardwarePlatform.GetSoftwarePlatform()); + pebble.InstallClient.InstallAppAsync(bundle, progress).Wait(); + + //for firmware v3, launch is done as part of the install + //Console.WriteLine("App Installed, launching..."); + //var uuid=new UUID(bundle.AppInfo.UUID); + //pebble.LaunchApp(uuid); + //Console.WriteLine ("Launched"); + } } } + else + { + Console.WriteLine("No .pbw"); + } + } + + private static void SendAppMessage(Pebble pebble) + { + string uuidAppPath = SelectApp(); + + if (!string.IsNullOrEmpty(uuidAppPath) && File.Exists(uuidAppPath)) + { + using (var stream = new FileStream(uuidAppPath, FileMode.Open)) + { + using (var zip = new Zip()) + { + zip.Open(stream); + var bundle = new AppBundle(); + stream.Position = 0; + bundle.Load(zip,pebble.Firmware.HardwarePlatform.GetSoftwarePlatform()); + + System.Console.Write("Enter Message:"); + var messageText = System.Console.ReadLine(); + + //format a message + var rand = new Random().Next(); + AppMessagePacket message = new AppMessagePacket(); + message.Command = (byte)Command.Push; + message.Values.Add(new AppMessageUInt32() { Key=0,Value = (uint)rand }); + message.Values.Add(new AppMessageString() { Key=1,Value = messageText }); + message.ApplicationId = bundle.AppMetadata.UUID; + message.TransactionId = 255; + + + //send it + Console.WriteLine("Sending Status "+rand+" to " + bundle.AppMetadata.UUID.ToString()); + var task = pebble.SendApplicationMessage(message); + task.Wait(); + Console.WriteLine("Response received"); + } + } + + + + } + else + { + Console.WriteLine("No .pbw"); + } } private static void ShowMediaCommands( Pebble pebble ) @@ -108,5 +233,10 @@ private static void DisplayResult( T result, Func successData ) Console.WriteLine( result.ErrorDetails.ToString() ); } } + + private static void ReceiveAppMessage(AppMessagePacket response) + { + System.Console.WriteLine("Recieved App Message"); + } } } diff --git a/PebbleSharp.Core.Tests/AbstractTests/BaseCrc32Tests.cs b/PebbleSharp.Core.Tests/AbstractTests/BaseCrc32Tests.cs index d21766b..746025d 100644 --- a/PebbleSharp.Core.Tests/AbstractTests/BaseCrc32Tests.cs +++ b/PebbleSharp.Core.Tests/AbstractTests/BaseCrc32Tests.cs @@ -15,7 +15,7 @@ public abstract class BaseCrc32Tests protected void RunGeneratesCorrectChecksumForApp() { var bundle = new AppBundle(); - bundle.Load(ResourceManager.GetAppBundle(), GetZip()); + bundle.Load(GetZip(),SoftwarePlatform.UNKNOWN); Assert.AreEqual(bundle.Manifest.Application.CRC, Crc32.Calculate(bundle.App)); } @@ -23,7 +23,7 @@ protected void RunGeneratesCorrectChecksumForApp() protected void RunGeneratesCorrectChecksumForFirmware() { var bundle = new FirmwareBundle(); - bundle.Load(ResourceManager.GetFirmwareBundle(), GetZip()); + bundle.Load(GetZip(),SoftwarePlatform.UNKNOWN); Assert.AreEqual(bundle.Manifest.Firmware.CRC, Crc32.Calculate(bundle.Firmware)); } diff --git a/PebbleSharp.Core.Tests/AbstractTests/BasePebbleBundleTests.cs b/PebbleSharp.Core.Tests/AbstractTests/BasePebbleBundleTests.cs index 7b91500..6979aad 100644 --- a/PebbleSharp.Core.Tests/AbstractTests/BasePebbleBundleTests.cs +++ b/PebbleSharp.Core.Tests/AbstractTests/BasePebbleBundleTests.cs @@ -18,7 +18,9 @@ protected void RunCanLoadInformationFromAppBundle() { Stream testBundle = ResourceManager.GetAppBundle(); var bundle = new AppBundle(); - bundle.Load( testBundle, GetZip() ); + var zip = GetZip(); + zip.Open(testBundle); + bundle.Load(zip,SoftwarePlatform.APLITE ); var manifest = bundle.Manifest; Assert.IsNotNull( manifest ); @@ -40,7 +42,6 @@ protected void RunCanLoadInformationFromAppBundle() Assert.AreEqual( (uint)0, bundle.AppMetadata.IconResourceID ); Assert.AreEqual( (uint)552, bundle.AppMetadata.Offset ); Assert.AreEqual( (uint)2, bundle.AppMetadata.RelocationListItemCount ); - Assert.AreEqual( (uint)3860, bundle.AppMetadata.RelocationListStart ); Assert.AreEqual( 3, bundle.AppMetadata.SDKMajorVersion ); Assert.AreEqual( 1, bundle.AppMetadata.SDKMinorVersion ); Assert.AreEqual( "3.1", bundle.AppMetadata.SDKVersion ); @@ -57,7 +58,9 @@ protected void RunCanLoadInformationFromFirmwareBundle() Stream testBundle = ResourceManager.GetFirmwareBundle(); var bundle = new FirmwareBundle(); - bundle.Load( testBundle, GetZip() ); + var zip = GetZip(); + zip.Open(testBundle); + bundle.Load( zip,SoftwarePlatform.APLITE ); Assert.IsNotNull( bundle.Firmware ); diff --git a/PebbleSharp.Core.Tests/PebbleSharp.Core.Tests.csproj b/PebbleSharp.Core.Tests/PebbleSharp.Core.Tests.csproj index e8d1edc..3a8e0e5 100644 --- a/PebbleSharp.Core.Tests/PebbleSharp.Core.Tests.csproj +++ b/PebbleSharp.Core.Tests/PebbleSharp.Core.Tests.csproj @@ -58,7 +58,6 @@ - diff --git a/PebbleSharp.Core.Tests/PebbleTests.cs b/PebbleSharp.Core.Tests/PebbleTests.cs index d310842..d5c3616 100644 --- a/PebbleSharp.Core.Tests/PebbleTests.cs +++ b/PebbleSharp.Core.Tests/PebbleTests.cs @@ -32,7 +32,7 @@ public async Task InstallFirmwareAsyncRequiresBundle() var bluetoothConnection = new Mock(); var pebble = new TestPebble( bluetoothConnection.Object, TEST_PEBBLE_ID ); - await pebble.InstallFirmwareAsync( null ); + await pebble.InstallClient.InstallFirmwareAsync( null ); } [TestMethod] @@ -75,7 +75,7 @@ public async Task InstallFirmwareAsyncTest() var pebble = new TestPebble( bluetoothConnection.Object, TEST_PEBBLE_ID ); - bool success = await pebble.InstallFirmwareAsync( bundle.Object ); + bool success = await pebble.InstallClient.InstallFirmwareAsync( bundle.Object ); Assert.IsTrue( success ); bluetoothConnection.Verify(); } diff --git a/PebbleSharp.Core/AppInfo.cs b/PebbleSharp.Core/AppInfo.cs new file mode 100644 index 0000000..2d78c3f --- /dev/null +++ b/PebbleSharp.Core/AppInfo.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Runtime.Serialization; + +namespace PebbleSharp.Core +{ + [DataContract] + public class AppInfo + { + //does exist in sdk3 + [DataMember(Name = "versionCode", IsRequired = false)] + public int VersionCode { get; private set; } + + [DataMember(Name = "sdkVersion", IsRequired = true)] + public string SdkVersion { get; private set; } + + //[DataMember(Name = "capabilities", IsRequired = true)] + //public string[] Capabilities { get; private set; } + + [DataMember(Name = "shortName", IsRequired = true)] + public string ShortName { get; private set; } + + /// The manifest for the resources contained in this bundle. + //resources + + //appKeys + //[DataMember(Name = "appKeys", IsRequired = true)] + //public Dictionary AppKeys { get; set; } + + [DataMember(Name = "uuid", IsRequired = true)] + public string UUID { get; private set; } + + [DataMember(Name = "versionLabel", IsRequired = true)] + public string VersionLabel { get; private set; } + + [DataMember(Name = "longName", IsRequired = true)] + public string LongName { get; private set; } + + //watchapp + + [DataMember(Name = "projectType", IsRequired = true)] + public string ProjectType { get; private set; } + } +} \ No newline at end of file diff --git a/PebbleSharp.Core/AppMessage/AppMessagePacket.cs b/PebbleSharp.Core/AppMessage/AppMessagePacket.cs new file mode 100644 index 0000000..599baed --- /dev/null +++ b/PebbleSharp.Core/AppMessage/AppMessagePacket.cs @@ -0,0 +1,490 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using PebbleSharp.Core.Responses; + +namespace PebbleSharp.Core.AppMessage +{ + //modeled after https://github.com/pebble/libpebble2/blob/master/libpebble2/services/appmessage.py + [Endpoint(Endpoint.ApplicationMessage)] + public class AppMessagePacket: ResponseBase + { + //TODO: the key names are in the manifest.... do we need them for anything? + + //byte layout is as follows + //command (always 1?) byte + //transactionid byte + //uuid byte[16] + //tuplecount byte + //tuple + // k uint key + // t byte type + // l ushort length + + //1:250:34:162:123:154:11:7:71:175:173:135:178:194:147:5:186:182:1:0:0:0:0:2:1:0:1 + //sample message + //1: command + //250: transaction id + //34:162:123:154:11:7:71:175:173:135:178:194:147:5:186:182: //uuid + //1: //tuple count + //0:0:0:0: //first tuple, key + //2: //type + //1:0: //length + //1 //value + + //sample outbound message + //1: + //0: + //11:7:71:175:173:135:178:194:147:5:186:182:255:255:255:255: + //1: + //0:0:0:0: + //2: + //4:0: + //87:195:209:30: + + public byte Command { get; set; } + + public byte TransactionId { get; set; } + public UUID ApplicationId { get; set; } + + public AppMessagePacket() + { + Values = new List(); + } + + public AppMessagePacket(byte[] bytes) + { + Load(bytes); + } + + protected override void Load(byte[] bytes) + { + Values = new List(); + int index = 0; + var command = bytes[index]; + index++; + + Command = command; + + TransactionId = bytes[index]; + index++; + + ApplicationId = new UUID(bytes.Skip(index).Take(16).ToArray()); + index += 16; + + int tupleCount = bytes[index]; + index++; + + for (int i = 0; i < tupleCount; i++) + { + uint k; + byte t; + ushort l; + + k = BitConverter.ToUInt32(bytes, index); + index += 4; + + t = bytes[index]; + index++; + + l = BitConverter.ToUInt16(bytes, index); + index += 2; + + IAppMessageDictionaryEntry entry = null; + if (t == (byte)PackedType.Bytes) + { + entry = new AppMessageBytes() { Value = bytes.Skip(index).Take(l).ToArray() }; + } + else if (t == (byte)PackedType.Signed) + { + if (l == 1) + { + entry = new AppMessageInt8() { Value = Convert.ToSByte(bytes[index]) }; + } + else if (l == 2) + { + entry = new AppMessageInt16() { Value = BitConverter.ToInt16(bytes, index) }; + } + else if (l == 4) + { + entry = new AppMessageInt32() { Value = BitConverter.ToInt32(bytes, index) }; + } + else + { + throw new PebbleException("Invalid signed integer length"); + } + } + else if (t == (byte)PackedType.String) + { + entry = new AppMessageString() { Value = System.Text.Encoding.UTF8.GetString(bytes, index, l) }; + } + else if (t == (byte)PackedType.Unsigned) + { + if (l == 1) + { + entry = new AppMessageUInt8() { Value = bytes[index] }; + } + else if (l == 2) + { + entry = new AppMessageUInt16() { Value = BitConverter.ToUInt16(bytes, index) }; + } + else if (l == 4) + { + entry = new AppMessageUInt32() { Value = BitConverter.ToUInt32(bytes, index) }; + } + else + { + throw new PebbleException("Invalid signed integer length"); + } + } + else + { + throw new PebbleException("Unknown tuple type"); + } + index += l; + entry.Key = k; + Values.Add(entry); + } + } + + public IList Values { get; set; } + + public byte[] GetBytes() + { + if (Values != null && Values.Any()) + { + var bytes = new List(); + bytes.Add(Command); + bytes.Add(TransactionId); + bytes.AddRange(ApplicationId.Data); + bytes.Add((byte)Values.Count); + foreach (var tuple in Values) + { + bytes.AddRange(tuple.PackedBytes); + } + return bytes.ToArray(); + } + else + { + return new byte[0]; + } + } + } + + public interface IAppMessageDictionaryEntry + { + uint Key { get; set; } + PackedType PackedType { get; } + ushort Length { get; } + byte[] ValueBytes { get; set; } + byte[] PackedBytes { get; } + } + + public enum Command:byte + { + Push=1 + } + + public enum PackedType:byte + { + Bytes=0, + String =1, + Unsigned =2, + Signed = 3 + } + + public abstract class AppMessageDictionaryEntry : IAppMessageDictionaryEntry + { + public uint Key { get; set; } + public abstract PackedType PackedType { get; } + + public virtual ushort Length + { + get { return (ushort) System.Runtime.InteropServices.Marshal.SizeOf(typeof(T)); } + } + + public virtual T Value { get; set; } + public abstract byte[] ValueBytes { get; set; } + + public byte[] PackedBytes + { + get + { + var bytes = new List(); + bytes.AddRange(BitConverter.GetBytes(Key)); + bytes.Add((byte)PackedType); + bytes.AddRange(BitConverter.GetBytes(Length)); + bytes.AddRange(ValueBytes); + return bytes.ToArray(); + } + } + + public new virtual string ToString() + { + return System.Convert.ToString(Value); + } + } + + public class AppMessageUInt8 : AppMessageDictionaryEntry + { + public override PackedType PackedType + { + get { return PackedType.Unsigned; } + } + + public override byte[] ValueBytes + { + get { return new byte[] {Value}; } + set + { + if (value == null) + { + throw new ArgumentNullException("value"); + } + else if (value.Length == Length) + { + Value = value[0]; + } + else + { + throw new PebbleException("Incorrect # of bytes"); + } + } + } + } + + public class AppMessageUInt16 : AppMessageDictionaryEntry + { + public override PackedType PackedType + { + get { return PackedType.Unsigned; } + } + + public override byte[] ValueBytes + { + get { return BitConverter.GetBytes(Value); } + set + { + if (value == null) + { + throw new ArgumentNullException("value"); + } + else if (value.Length == Length) + { + Value = BitConverter.ToUInt16(value,0); + } + else + { + throw new PebbleException("Incorrect # of bytes"); + } + } + } + } + + public class AppMessageUInt32 : AppMessageDictionaryEntry + { + public override PackedType PackedType + { + get { return PackedType.Unsigned; } + } + + public override byte[] ValueBytes + { + get { return BitConverter.GetBytes(Value); } + set + { + if (value == null) + { + throw new ArgumentNullException("value"); + } + else if (value.Length == Length) + { + Value = BitConverter.ToUInt32(value, 0); + } + else + { + throw new PebbleException("Incorrect # of bytes"); + } + } + } + } + + public class AppMessageInt8 : AppMessageDictionaryEntry + { + public override PackedType PackedType + { + get { return PackedType.Signed; } + } + + public override byte[] ValueBytes + { + get { return new byte[] { Convert.ToByte(Value) }; } + set + { + if (value == null) + { + throw new ArgumentNullException("value"); + } + else if (value.Length == Length) + { + Value = Convert.ToSByte(value); + } + else + { + throw new PebbleException("Incorrect # of bytes"); + } + } + } + } + + public class AppMessageInt16 : AppMessageDictionaryEntry + { + public override PackedType PackedType + { + get { return PackedType.Signed; } + } + + public override byte[] ValueBytes + { + get { return BitConverter.GetBytes(Value); } + set + { + if (value == null) + { + throw new ArgumentNullException("value"); + } + else if (value.Length == Length) + { + Value = BitConverter.ToInt16(value, 0); + } + else + { + throw new PebbleException("Incorrect # of bytes"); + } + } + } + } + + public class AppMessageInt32 : AppMessageDictionaryEntry + { + public override PackedType PackedType + { + get { return PackedType.Signed; } + } + + public override byte[] ValueBytes + { + get { return BitConverter.GetBytes(Value); } + set + { + if (value == null) + { + throw new ArgumentNullException("value"); + } + else if (value.Length == Length) + { + Value = BitConverter.ToInt32(value, 0); + } + else + { + throw new PebbleException("Incorrect # of bytes"); + } + } + } + } + + public class AppMessageString : AppMessageDictionaryEntry + { + public override PackedType PackedType + { + get { return PackedType.String; } + } + + public override ushort Length + { + get { return (ushort)ValueBytes.Length; } + } + + public override string Value + { + get + { + return base.Value; + } + set + { + if (value != null && value.EndsWith("\0")) + { + base.Value = value.Substring(0, value.Length - 1); + } + else + { + base.Value = value; + } + } + } + + public override byte[] ValueBytes + { + get + { + return System.Text.UTF8Encoding.UTF8.GetBytes(Value+"\0"); + } + set + { + if (value == null) + { + throw new ArgumentNullException("value"); + } + else if (value.Length <= ushort.MaxValue) + { + Value = System.Text.UTF8Encoding.UTF8.GetString(value,0,value.Length); + } + else + { + throw new OverflowException("Specified string is too large for length to fit in a ushort"); + } + } + } + } + + public class AppMessageBytes : AppMessageDictionaryEntry + { + public override PackedType PackedType + { + get { return PackedType.Bytes; } + } + + public override ushort Length + { + get { return (ushort)Value.Length; } + } + + public override byte[] ValueBytes + { + get { return Value; } + set + { + if (value == null) + { + throw new ArgumentNullException("value"); + } + else if (value.Length <= ushort.MaxValue) + { + Value = value; + } + else + { + throw new OverflowException("Specified array is too large for length to fit in a ushort"); + } + } + } + + public override string ToString() + { + return BitConverter.ToString(Value).Replace("-", "").ToLower(); + } + } +} diff --git a/PebbleSharp.Core/ApplicationManifest.cs b/PebbleSharp.Core/ApplicationManifest.cs index 14d016b..e0f9c46 100644 --- a/PebbleSharp.Core/ApplicationManifest.cs +++ b/PebbleSharp.Core/ApplicationManifest.cs @@ -12,7 +12,7 @@ public struct ApplicationManifest public string Filename { get; private set; } /// The firmware version required to run this application. - [DataMember(Name = "reqFwVer", IsRequired = true)] + [DataMember(Name = "reqFwVer", IsRequired = false)] public int RequiredFirmwareVersion { get; private set; } /// The time at which the application binary was created. (?) diff --git a/PebbleSharp.Core/ApplicationMetadata.cs b/PebbleSharp.Core/ApplicationMetadata.cs index 72928f2..79cde3f 100644 --- a/PebbleSharp.Core/ApplicationMetadata.cs +++ b/PebbleSharp.Core/ApplicationMetadata.cs @@ -23,6 +23,27 @@ public string StructVersion get { return string.Format("{0}.{1}", StructMajorVersion, StructMinorVersion); } } + /* + source: https://github.com/pebble/libpebble2/blob/d9ecce4a345f31217fb510f2f4e840f7cdda235b/libpebble2/util/bundle.py + python struct packing: https://docs.python.org/2/library/struct.html + STRUCT_DEFINITION = [ + '8s', # header = char[8] + '2B', # struct version = byte[2] + '2B', # sdk version = byte[2] + '2B', # app version =byte[2] + 'H', # size = ushort + 'I', # offset = uint + 'I', # crc = uint + '32s', # app name = char[32] + '32s', # company name = char[32] + 'I', # icon resource id = uint + 'I', # symbol table address = uint + 'I', # flags = uint + 'I', # num relocation list entries = uint + '16s' # uuid = char[16] + ] + */ + // The data as stored in the binary [Serializable(Order = 0, Size = 8)] public string Header { get; set; } @@ -54,11 +75,11 @@ public string StructVersion public uint SymbolTableAddress { get; set; } [Serializable(Order = 14)] public uint Flags { get; set; } + //[Serializable(Order = 15)] + //public uint RelocationListStart { get; set; } [Serializable(Order = 15)] - public uint RelocationListStart { get; set; } - [Serializable(Order = 16)] public uint RelocationListItemCount { get; set; } - [Serializable(Order = 17)] + [Serializable(Order = 16)] public UUID UUID { get; set; } public override string ToString() diff --git a/PebbleSharp.Core/Apps/AppFetchRequestPacket.cs b/PebbleSharp.Core/Apps/AppFetchRequestPacket.cs new file mode 100644 index 0000000..5b60ca1 --- /dev/null +++ b/PebbleSharp.Core/Apps/AppFetchRequestPacket.cs @@ -0,0 +1,38 @@ +using System; +using System.Linq; +using PebbleSharp.Core.Responses; +namespace PebbleSharp.Core +{ + [Endpoint(Endpoint.AppFetch)] + public class AppFetchRequestPacket:ResponseBase + { + public byte Command { get; set; } + public UUID UUID { get; set; } + public int AppId { get; set; } + + public AppFetchRequestPacket() + { + } + + protected override void Load(byte[] payload) + { + Command = payload[0]; + UUID = new UUID(payload.Skip(1).Take(16).ToArray()); + + + //this packet is defined as little endian, which is slightly abnormal since + //it is coming from the pebble + //(most packets from he pebble are big endian / network endian) + //TODO: refactor Util conversions to respect per packet endian attributes + if (BitConverter.IsLittleEndian) + { + AppId = BitConverter.ToInt32(payload, 17); + } + else + { + AppId = BitConverter.ToInt32(payload.Skip(16).Take(4).ToArray().Reverse().ToArray(),0); + } + } + } +} + diff --git a/PebbleSharp.Core/Apps/AppFetchResponsePacket.cs b/PebbleSharp.Core/Apps/AppFetchResponsePacket.cs new file mode 100644 index 0000000..7ba66c1 --- /dev/null +++ b/PebbleSharp.Core/Apps/AppFetchResponsePacket.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using PebbleSharp.Core.Responses; +namespace PebbleSharp.Core +{ + [Endpoint(Endpoint.AppFetch)] + public class AppFetchResponsePacket + { + public byte Command { get; set; } + public AppFetchStatus Response { get; set; } + + public AppFetchResponsePacket() + { + Command = 1; + } + + public byte[] GetBytes() + { + return new byte[]{Command,(byte)Response}; + } + } +} + diff --git a/PebbleSharp.Core/Apps/AppRunStatePacket.cs b/PebbleSharp.Core/Apps/AppRunStatePacket.cs new file mode 100644 index 0000000..a42f954 --- /dev/null +++ b/PebbleSharp.Core/Apps/AppRunStatePacket.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using PebbleSharp.Core.Responses; +namespace PebbleSharp.Core +{ + [Endpoint( Endpoint.AppRunState)] + public class AppRunStatePacket + { + public AppRunState Command { get; set; } + public UUID UUID { get; set; } + + public AppRunStatePacket() + { + } + + public byte[] GetBytes() + { + var bytes = new List(); + bytes.Add((byte)Command); + if (Command == AppRunState.Start || Command == AppRunState.Stop) + { + bytes.AddRange(UUID.Data); + } + return bytes.ToArray(); + } + } +} + diff --git a/PebbleSharp.Core/BlobDB/AppMetaData.cs b/PebbleSharp.Core/BlobDB/AppMetaData.cs new file mode 100644 index 0000000..4b6a087 --- /dev/null +++ b/PebbleSharp.Core/BlobDB/AppMetaData.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using PebbleSharp.Core.Bundles; + +namespace PebbleSharp.Core.BlobDB +{ + public class AppMetaData + { + public UUID UUID { get; set; } + public UInt32 Flags { get; set; } + public UInt32 Icon { get; set; } + public byte AppVersionMajor { get; set; } + public byte AppVersionMinor { get; set; } + public byte SdkVersionMajor { get; set; } + public byte SdkVersionMinor { get; set; } + public byte AppFaceBackgroundColor { get; set; } + public byte AppFaceTemplateId { get; set; } + public string Name { get; set; }/*Fixed length 96*/ + + public byte[] GetBytes() + { + var bytes = new List(); + bytes.AddRange(this.UUID.Data); + bytes.AddRange(BitConverter.GetBytes(Flags)); + bytes.AddRange(BitConverter.GetBytes(Icon)); + bytes.Add(AppVersionMajor); + bytes.Add(AppVersionMinor); + bytes.Add(SdkVersionMajor); + bytes.Add(SdkVersionMinor); + bytes.Add(AppFaceBackgroundColor); + bytes.Add(AppFaceTemplateId); + var name = Name; + + //TODO: build "fixed" type strings into pebblesharp core + if (name.Length > 96) + { + name = name.Substring(0, 96); + } + name = name.PadRight(96, '\0'); + + var nameBytes = Util.GetBytes(name, false); + + bytes.AddRange(nameBytes); + return bytes.ToArray(); + } + + public static AppMetaData FromAppBundle(AppBundle bundle, byte appFaceTemplateId = 0, byte appFaceBackgroundColor = 0) + { + var meta = new PebbleSharp.Core.BlobDB.AppMetaData(); + meta.AppFaceTemplateId = appFaceTemplateId; + meta.AppFaceBackgroundColor = appFaceBackgroundColor; + meta.AppVersionMajor = bundle.AppMetadata.AppMajorVersion; + meta.AppVersionMinor = bundle.AppMetadata.AppMinorVersion; + meta.SdkVersionMajor = bundle.AppMetadata.SDKMajorVersion; + meta.SdkVersionMinor = bundle.AppMetadata.SDKMinorVersion; + meta.Flags = bundle.AppMetadata.Flags; + meta.Icon = bundle.AppMetadata.IconResourceID; + meta.UUID = bundle.AppMetadata.UUID; + meta.Name = bundle.AppMetadata.AppName; + + return meta; + } + } +} + diff --git a/PebbleSharp.Core/BlobDB/BlobDBClient.cs b/PebbleSharp.Core/BlobDB/BlobDBClient.cs new file mode 100644 index 0000000..c590f94 --- /dev/null +++ b/PebbleSharp.Core/BlobDB/BlobDBClient.cs @@ -0,0 +1,65 @@ +using System; +using System.Threading.Tasks; + +namespace PebbleSharp.Core.BlobDB +{ + public class BlobDBClient + { + private Pebble _pebble; + private static Random _random; + + static BlobDBClient() + { + _random = new Random(); + } + + internal BlobDBClient(Pebble pebble) + { + _pebble = pebble; + } + + public async Task Insert(BlobDatabase database, byte[] key, byte[] value) + { + var insertCommand = new BlobDBCommandPacket() + { + Token = GenerateToken(), + Database = database, + Command = BlobCommand.Insert, + Key = key, + Value = value + }; + return await Send(insertCommand); + } + public async Task Delete(BlobDatabase database, byte[] key) + { + var deleteCommand = new BlobDBCommandPacket() + { + Token = GenerateToken(), + Database = database, + Command = BlobCommand.Delete, + Key = key, + }; + return await Send(deleteCommand); + } + public async Task Clear(BlobDatabase database) + { + var clearCommand = new BlobDBCommandPacket() + { + Token = GenerateToken(), + Database = database, + Command = BlobCommand.Clear + }; + return await Send(clearCommand); + } + private async Task Send(BlobDBCommandPacket command) + { + return await _pebble.SendBlobDBMessage(command); + } + public ushort GenerateToken() + { + //this is how libpebble2 does it...random.randrange(1, 2**16 - 1, 1) + return (ushort)_random.Next(1, (2 ^ 16) - 1); + } + } +} + diff --git a/PebbleSharp.Core/BlobDB/BlobDBCommandPacket.cs b/PebbleSharp.Core/BlobDB/BlobDBCommandPacket.cs new file mode 100644 index 0000000..efb0b0d --- /dev/null +++ b/PebbleSharp.Core/BlobDB/BlobDBCommandPacket.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using PebbleSharp.Core; +using PebbleSharp.Core.Responses; + +namespace PebbleSharp.Core.BlobDB +{ + [Endpoint(Endpoint.BlobDB)] + public class BlobDBCommandPacket + { + public BlobCommand Command {get;set;} + public ushort Token { get; set; } + public BlobDatabase Database { get; set; } + + //only used for insert and delete commands + public byte[] Key { get; set; } + public byte KeyLength + { + get + { + return (byte)Key.Length; + } + } + + //only used for insert + public byte[] Value { get; set; } + public ushort ValueLength + { + get + { + return (ushort)Value.Length; + } + } + + public byte[] GetBytes() + { + var bytes = new List(); + bytes.Add((byte)Command); + bytes.AddRange(BitConverter.GetBytes(Token)); + bytes.Add((byte)Database); + if (Command == BlobCommand.Insert || Command == BlobCommand.Delete) + { + bytes.Add(KeyLength); + bytes.AddRange(Key); + if (Command == BlobCommand.Insert) + { + bytes.AddRange(BitConverter.GetBytes(ValueLength)); + bytes.AddRange(Value); + } + } + return bytes.ToArray(); + } + } +} \ No newline at end of file diff --git a/PebbleSharp.Core/BlobDB/BlobDBResponsePacket.cs b/PebbleSharp.Core/BlobDB/BlobDBResponsePacket.cs new file mode 100644 index 0000000..a09c91a --- /dev/null +++ b/PebbleSharp.Core/BlobDB/BlobDBResponsePacket.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using PebbleSharp.Core; +using PebbleSharp.Core.Responses; + +namespace PebbleSharp.Core.BlobDB +{ + [Endpoint(Endpoint.BlobDB)] + public class BlobDBResponsePacket :ResponseBase + { + public ushort Token { get; private set;} + public BlobStatus Response { get; private set; } + + public byte[] Payload { get; private set; } + //token = Uint16() + //response = Uint8(enum=BlobStatus) + + protected override void Load( byte[] payload ) + { + if (payload.Length == 0) + { + SetError("BlobDB Command failed"); + } + Payload = payload; + Token = BitConverter.ToUInt16(payload, 0); + Response = (BlobStatus)payload[2]; + } + } + + +} + diff --git a/PebbleSharp.Core/BlobDB/TimelineAction.cs b/PebbleSharp.Core/BlobDB/TimelineAction.cs new file mode 100644 index 0000000..c20dae5 --- /dev/null +++ b/PebbleSharp.Core/BlobDB/TimelineAction.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +namespace PebbleSharp.Core +{ + public class TimelineAction + { + public enum TimelineActionType:byte + { + AncsDismiss = 0x01, + Generic = 0x02, + Response = 0x03, + Dismiss = 0x04, + HTTP = 0x05, + Snooze = 0x06, + OpenWatchapp = 0x07, + Empty = 0x08, + Remove = 0x09, + OpenPin = 0x0a, + } + public byte ActionId { get; set; } + public TimelineActionType ActionType { get; set; } + public byte AttributeCount + { + get + { + if (Attributes != null) + { + return (byte)Attributes.Count; + } + else + { + return 0; + } + } + } + public IList Attributes { get; set; } + + public TimelineAction() + { + + } + + public byte[] GetBytes() + { + var bytes = new List(); + bytes.Add(ActionId); + bytes.Add((byte)ActionType); + bytes.Add(AttributeCount); + if (Attributes != null) + { + foreach (var attribute in Attributes) + { + bytes.AddRange(attribute.GetBytes()); + } + } + return bytes.ToArray(); + } + } +} + diff --git a/PebbleSharp.Core/BlobDB/TimelineAttribute.cs b/PebbleSharp.Core/BlobDB/TimelineAttribute.cs new file mode 100644 index 0000000..af83525 --- /dev/null +++ b/PebbleSharp.Core/BlobDB/TimelineAttribute.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +namespace PebbleSharp.Core +{ + public class TimelineAttribute + { + public byte AttributeId { get; set; } + public ushort Length + { + get + { + if (Content != null) + { + return (ushort)Content.Length; + } + else + { + return 0; + } + } + } + public byte[] Content { get; set; } + public TimelineAttribute() + { + } + + public byte[] GetBytes() + { + var bytes = new List(); + bytes.Add(AttributeId); + bytes.AddRange(BitConverter.GetBytes(Length)); + bytes.AddRange(Content); + return bytes.ToArray(); + } + } +} + diff --git a/PebbleSharp.Core/BlobDB/TimelineItem.cs b/PebbleSharp.Core/BlobDB/TimelineItem.cs new file mode 100644 index 0000000..74eed12 --- /dev/null +++ b/PebbleSharp.Core/BlobDB/TimelineItem.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections.Generic; + +namespace PebbleSharp.Core +{ + public class TimelineItem + { + public enum TimelineItemType:byte + { + Notification=1, + Pin=2, + Reminder=3 + } + + public UUID ItemId { get; set; } + public UUID ParentId { get; set; } + public DateTime TimeStamp { get; set; } + public ushort Duration { get; set; } + public TimelineItemType ItemType { get; set; } + public ushort Flags { get; set; } + public byte Layout { get; set; } + public ushort DataLength { get; private set; } + public byte AttributeCount + { + get + { + if (Attributes != null) + { + return (byte)Attributes.Count; + } + else + { + return 0; + } + } + } + public byte ActionCount + { + get + { + if (Actions != null) + { + return (byte)Actions.Count; + } + else + { + return 0; + } + } + } + public IList Attributes { get; set; } + public IList Actions { get; set; } + + public TimelineItem() + { + } + + public byte[] GetBytes() + { + var bytes = new List(); + bytes.AddRange(ItemId.Data); + bytes.AddRange(ParentId.Data); + bytes.AddRange(BitConverter.GetBytes(Util.GetTimestampFromDateTime(this.TimeStamp))); + bytes.AddRange(BitConverter.GetBytes(Duration)); + bytes.Add((byte)this.ItemType); + bytes.AddRange(BitConverter.GetBytes(this.Flags)); + bytes.Add(Layout); + + var attributeBytes = new List(); + if (Attributes != null) + { + foreach (var attribute in Attributes) + { + attributeBytes.AddRange(attribute.GetBytes()); + } + } + + var actionBytes = new List(); + if (Actions != null) + { + foreach (var action in Actions) + { + actionBytes.AddRange(action.GetBytes()); + } + } + + this.DataLength = (ushort)(attributeBytes.Count + actionBytes.Count); + bytes.AddRange(BitConverter.GetBytes(DataLength)); + bytes.Add(AttributeCount); + bytes.Add(ActionCount); + bytes.AddRange(attributeBytes); + bytes.AddRange(actionBytes); + return bytes.ToArray(); + } + } +} + diff --git a/PebbleSharp.Core/Bundles/AppBundle.cs b/PebbleSharp.Core/Bundles/AppBundle.cs index 367fc7d..2358849 100644 --- a/PebbleSharp.Core/Bundles/AppBundle.cs +++ b/PebbleSharp.Core/Bundles/AppBundle.cs @@ -1,6 +1,7 @@ using System; using System.IO; using PebbleSharp.Core.Serialization; +using System.Runtime.Serialization.Json; namespace PebbleSharp.Core.Bundles { @@ -9,13 +10,14 @@ public class AppBundle : BundleBase public byte[] App { get; private set; } public ApplicationMetadata AppMetadata { get; private set; } - + public PebbleSharp.Core.AppInfo AppInfo { get; private set; } + protected override void LoadData( IZip zip ) { if ( string.IsNullOrWhiteSpace( Manifest.Application.Filename ) ) - throw new InvalidOperationException( "Bundle does not contain pebble app" ); + throw new PebbleException("Bundle does not contain pebble app"); - using ( Stream binStream = zip.OpenEntryStream( Manifest.Application.Filename ) ) + using ( Stream binStream = zip.OpenEntryStream( this.PlatformSubdirectory()+Manifest.Application.Filename ) ) { if ( binStream == null ) throw new Exception( string.Format( "App file {0} not found in archive", Manifest.Application.Filename ) ); @@ -23,6 +25,15 @@ protected override void LoadData( IZip zip ) App = Util.GetBytes( binStream ); AppMetadata = BinarySerializer.ReadObject( App ); + } + //note, appinfo.json is NOT under the platform subdir + using (Stream appinfoStream = zip.OpenEntryStream("appinfo.json")) + { + if (appinfoStream != null) + { + var serializer = new DataContractJsonSerializer(typeof(PebbleSharp.Core.AppInfo)); + AppInfo = (PebbleSharp.Core.AppInfo)serializer.ReadObject(appinfoStream); + } } } diff --git a/PebbleSharp.Core/Bundles/BundleBase.cs b/PebbleSharp.Core/Bundles/BundleBase.cs index 4f44f40..7c59f78 100644 --- a/PebbleSharp.Core/Bundles/BundleBase.cs +++ b/PebbleSharp.Core/Bundles/BundleBase.cs @@ -10,35 +10,40 @@ public abstract class BundleBase public virtual bool HasResources { get; private set; } public BundleManifest Manifest { get; private set; } public virtual byte[] Resources { get; private set; } + public SoftwarePlatform Platform { get; private set;} protected abstract void LoadData(IZip zip); + protected string PlatformSubdirectory() + { + var platformSubdirectory = (Platform == SoftwarePlatform.UNKNOWN ? "" : Platform.ToString().ToLower()+"/"); + return platformSubdirectory; + } + + private BundleManifest LoadManifest(IZip zip) + { + using (var manifestStream = zip.OpenEntryStream(PlatformSubdirectory() + "manifest.json")) + { + var serializer = new DataContractJsonSerializer(typeof(BundleManifest)); + return (BundleManifest)serializer.ReadObject(manifestStream); + } + } + /// /// Create a new PebbleBundle from a .pwb file and parse its metadata. /// /// The stream to the bundle. /// The zip library implementation. - public void Load(Stream bundle, IZip zip) + public void Load(IZip zip, SoftwarePlatform platform) { - //TODO: This needs to be refactored, probably put into a Load method - if (false == zip.Open(bundle)) - throw new InvalidOperationException("Failed to open pebble bundle"); - - using (Stream manifestStream = zip.OpenEntryStream("manifest.json")) - { - if (manifestStream == null) - { - throw new InvalidOperationException("manifest.json not found in archive - not a valid Pebble bundle."); - } - var serializer = new DataContractJsonSerializer(typeof(BundleManifest)); - Manifest = (BundleManifest)serializer.ReadObject(manifestStream); - } + Platform = platform; + Manifest = LoadManifest (zip); HasResources = (Manifest.Resources.Size != 0); if (HasResources) { - using (Stream resourcesBinary = zip.OpenEntryStream(Manifest.Resources.Filename)) + using (Stream resourcesBinary = zip.OpenEntryStream(PlatformSubdirectory()+Manifest.Resources.Filename)) { if (resourcesBinary == null) throw new PebbleException("Could not find resource entry in the bundle"); diff --git a/PebbleSharp.Core/Bundles/FirmwareBundle.cs b/PebbleSharp.Core/Bundles/FirmwareBundle.cs index 6380116..167c287 100644 --- a/PebbleSharp.Core/Bundles/FirmwareBundle.cs +++ b/PebbleSharp.Core/Bundles/FirmwareBundle.cs @@ -10,7 +10,7 @@ public class FirmwareBundle : BundleBase protected override void LoadData(IZip zip) { if (string.IsNullOrWhiteSpace(Manifest.Firmware.Filename)) - throw new InvalidOperationException("Bundle does not contain firmware"); + throw new PebbleException("Bundle does not contain firmware"); using (Stream binStream = zip.OpenEntryStream(Manifest.Firmware.Filename)) { diff --git a/PebbleSharp.Core/Enums.cs b/PebbleSharp.Core/Enums.cs index fc07815..fbd3e71 100644 --- a/PebbleSharp.Core/Enums.cs +++ b/PebbleSharp.Core/Enums.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; namespace PebbleSharp.Core { @@ -17,61 +18,44 @@ public enum MediaControl : byte SendNowPlaying = 9 } - /// - /// Endpoints (~"commands") used by Pebble to indicate particular instructions - /// or instruction types. - /// - public enum Endpoint : ushort - { - Firmware = 1, - Time = 11, - FirmwareVersion = 16, - PhoneVersion = 17, - SystemMessage = 18, - MusicControl = 32, - PhoneControl = 33, - ApplicationMessage = 48, - Launcher = 49, - AppCustomize = 50, - Logs = 2000, - Ping = 2001, - LogDump = 2002, - Reset = 2003, - App = 2004, - Mfg = 2004, - AppLogs = 2006, - Notification = 3000, - Resource = 4000, - SysReg = 5000, - FctReg = 5001, - AppManager = 6000, - RunKeeper = 7000, - PutBytes = 48879, - DataLog = 6778, - CoreDump = 9000, - MaxEndpoint = 65535 //ushort.MaxValue - } + /// + /// Endpoints (~"commands") used by Pebble to indicate particular instructions + /// or instruction types. + /// + public enum Endpoint : ushort + { + Firmware = 1, + Time = 11, + FirmwareVersion = 16, + PhoneVersion = 17, + SystemMessage = 18, + MusicControl = 32, + PhoneControl = 33, + ApplicationMessage = 48, + Launcher = 49, + AppCustomize = 50, + AppRunState = 52, + Logs = 2000, + Ping = 2001, + LogDump = 2002, + Reset = 2003, + App = 2004, + Mfg = 2004, + AppLogs = 2006, + Notification = 3000, + Resource = 4000, + SysReg = 5000, + FctReg = 5001, + AppManager = 6000, + AppFetch = 6001, + RunKeeper = 7000, + PutBytes = 48879, + DataLog = 6778, + CoreDump = 9000, + BlobDB = 45531,//0xb1db + MaxEndpoint = 65535, //ushort.MaxValue - [Flags] - public enum RemoteCaps : uint - { - Unknown = 0, - IOS = 1, - Android = 2, - OSX = 3, - Linux = 4, - Windows = 5, - Telephony = 16, - SMS = 32, - GPS = 64, - BTLE = 128, - // 240? No, that doesn't make sense. But it's apparently true. - CameraFront = 240, - CameraRear = 256, - Accelerometer = 512, - Gyro = 1024, - Compass = 2048 - } + } public enum LogLevel { @@ -84,11 +68,164 @@ public enum LogLevel Verbose = 250 } - public enum AppMessage : byte + public enum AppMessageCommand : byte { Push = 0x01, Request = 0x02, Ack = 0xFF, Nack = 0x7F } + + public enum BlobDatabase:byte + { + Test = 0, + Pin = 1, + App = 2, + Reminder = 3, + Notification = 4 + } + + public enum BlobStatus:byte + { + Success = 0x01, + GeneralFailure = 0x02, + InvalidOperation = 0x03, + InvalidDatabaseID = 0x04, + InvalidData = 0x05, + KeyDoesNotExist = 0x06, + DatabaseFull = 0x07, + DataStale = 0x08 + } + + public enum BlobCommand:byte + { + Insert=0x01, + Delete=0x04, + Clear=0x05, + } + + public enum AppRunState : byte + { + Start=0x01, + Stop=0x02, + Request=0x03, + } + + public enum AppFetchStatus : byte + { + Start = 0x01, + Busy = 0x02, + InvalidUUID = 0x03, + NoData = 0x04 + } + + + public enum TransferType : byte + { + Firmware = 1, + Recovery = 2, + SysResources = 3, + Resources = 4, + Binary = 5, + File=6, + Worker=7 + } + + public enum SystemMessage : byte + { + FirmwareAvailible = 0, + FirmwareStart = 1, + FirmwareComplete = 2, + FirmwareFail = 3, + FirmwareUpToDate = 4, + FirmwareOutOfDate = 5, + BluetoothStartDiscoverable = 6, + BluetoothEndDiscoverable = 7 + } + + public enum PutBytesType : byte + { + Init=0x01, + Put=0x02, + Commit=0x03, + Abort=0x04, + Install=0x05, + } + + public enum PutBytesResult : byte + { + Ack=0x01, + Nack=0x02 + } + + public enum ResetCommand + { + Reset = 0x00, + DumpCore = 0x01, + FactoryReset = 0x02, + PRF = 0x03 + } + + public enum Hardware:byte + { + UNKNOWN = 0, + TINTIN_EV1 = 1, + TINTIN_EV2 = 2, + TINTIN_EV2_3 = 3, + TINTIN_EV2_4 = 4, + TINTIN_V1_5 = 5, + BIANCA = 6, + SNOWY_EVT2 = 7, + SNOWY_DVT = 8, + SPALDING_EVT = 9, + BOBBY_SMILES = 10, + SPALDING = 11, + TINTIN_BB = 0xFF, + TINTIN_BB2 = 0xFE, + SNOWY_BB = 0xFD, + SNOWY_BB2 = 0xFC, + SPALDING_BB2 = 0xFB, + } + + //note that the names of these values are important, .ToString() + //is used to locate the correct platform specific subdirectory in appbundles + public enum SoftwarePlatform : byte + { + UNKNOWN, + APLITE, + BASALT, + CHALK + } + + public static class HardwareHelpers + { + private static Dictionary Platforms; + + static HardwareHelpers() + { + Platforms = new Dictionary(); + Platforms.Add(Hardware.UNKNOWN, SoftwarePlatform.UNKNOWN); + Platforms.Add(Hardware.TINTIN_EV1, SoftwarePlatform.APLITE); + Platforms.Add(Hardware.TINTIN_EV2, SoftwarePlatform.APLITE); + Platforms.Add(Hardware.TINTIN_EV2_3, SoftwarePlatform.APLITE); + Platforms.Add(Hardware.TINTIN_EV2_4, SoftwarePlatform.APLITE); + Platforms.Add(Hardware.TINTIN_V1_5, SoftwarePlatform.APLITE); + Platforms.Add(Hardware.BIANCA, SoftwarePlatform.APLITE); + Platforms.Add(Hardware.SNOWY_EVT2, SoftwarePlatform.BASALT); + Platforms.Add(Hardware.SNOWY_DVT, SoftwarePlatform.BASALT); + Platforms.Add(Hardware.BOBBY_SMILES, SoftwarePlatform.BASALT); + Platforms.Add(Hardware.SPALDING_EVT, SoftwarePlatform.CHALK); + Platforms.Add(Hardware.SPALDING, SoftwarePlatform.CHALK); + Platforms.Add(Hardware.TINTIN_BB, SoftwarePlatform.APLITE); + Platforms.Add(Hardware.TINTIN_BB2, SoftwarePlatform.APLITE); + Platforms.Add(Hardware.SNOWY_BB, SoftwarePlatform.BASALT); + Platforms.Add(Hardware.SNOWY_BB2, SoftwarePlatform.BASALT); + Platforms.Add(Hardware.SPALDING_BB2, SoftwarePlatform.CHALK); + } + + public static SoftwarePlatform GetSoftwarePlatform(this Hardware hardware) + { + return Platforms[hardware]; + } + } } \ No newline at end of file diff --git a/PebbleSharp.Core/FirmwareVersion.cs b/PebbleSharp.Core/FirmwareVersion.cs index 67b7576..1920580 100644 --- a/PebbleSharp.Core/FirmwareVersion.cs +++ b/PebbleSharp.Core/FirmwareVersion.cs @@ -1,11 +1,13 @@ using System; +using System.Collections; +using System.Collections.Generic; namespace PebbleSharp.Core { public class FirmwareVersion { public FirmwareVersion( DateTime timestamp, string version, string commit, - bool isRecovery, byte hardwarePlatform, byte metadataVersion ) + bool isRecovery, Hardware hardwarePlatform, byte metadataVersion ) { Timestamp = timestamp; Version = version; @@ -19,9 +21,27 @@ public FirmwareVersion( DateTime timestamp, string version, string commit, public string Version { get; private set; } public string Commit { get; private set; } public bool IsRecovery { get; private set; } - public byte HardwarePlatform { get; private set; } + public Hardware HardwarePlatform { get; private set; } public byte MetadataVersion { get; private set; } + public IList ParseVersionComponents() + { + var components = new List(); + if (!string.IsNullOrWhiteSpace(Version)) + { + string cleanedVersion = Version.Replace("v", ""); + foreach (var component in cleanedVersion.Split(new char[] {'.', '-'}, StringSplitOptions.RemoveEmptyEntries)) + { + int v; + if (int.TryParse(component, out v)) + { + components.Add(v); + } + } + } + return components; + } + public override string ToString() { const string format = "Version {0}, commit {1} ({2})\n" diff --git a/PebbleSharp.Core/Install/InstallClient.cs b/PebbleSharp.Core/Install/InstallClient.cs new file mode 100644 index 0000000..804c219 --- /dev/null +++ b/PebbleSharp.Core/Install/InstallClient.cs @@ -0,0 +1,202 @@ +using System; +using System.Threading.Tasks; +using System.Linq; +using System.Collections.Generic; +using PebbleSharp.Core.Responses; +using PebbleSharp.Core.Bundles; +using PebbleSharp.Core.BlobDB; + +namespace PebbleSharp.Core.Install +{ + public class InstallClient + { + private Pebble _pebble; + + internal InstallClient(Pebble pebble) + { + _pebble = pebble; + } + + public async Task InstallAppAsync( AppBundle bundle, IProgress progress = null ) + { + IList versionComponents = _pebble.Firmware.ParseVersionComponents(); + if (versionComponents[0] < 3) + { + await InstallAppLegacyV2 (bundle, progress); + } + else + { + await InstallAppAsyncV3 (bundle, progress); + } + } + + private async Task InstallAppAsyncV3(AppBundle bundle,IProgress progress) + { + //https://github.com/pebble/libpebble2/blob/master/libpebble2/services/install.py + + var meta = AppMetaData.FromAppBundle(bundle); + + var bytes = meta.GetBytes(); + var result = await _pebble.BlobDBClient.Delete(BlobDatabase.App, meta.UUID.Data); + + result = await _pebble.BlobDBClient.Insert(BlobDatabase.App, meta.UUID.Data, bytes); + + if (result.Response == BlobStatus.Success) + { + var startPacket = new AppRunStatePacket(); + startPacket.Command = AppRunState.Start; + startPacket.UUID = meta.UUID; + //app_fetch = self._pebble.send_and_read(AppRunState(data=AppRunStateStart(uuid=app_uuid)), AppFetchRequest) + + var runStateResult = await _pebble.SendMessageAsync(Endpoint.AppRunState, startPacket.GetBytes()); + + if (!runStateResult.Success) + { + throw new PebbleException("Pebble replied invalid run state"); + } + + if (!meta.UUID.Equals(runStateResult.UUID)) + { + var response = new AppFetchResponsePacket(); + response.Response = AppFetchStatus.InvalidUUID; + await _pebble.SendMessageNoResponseAsync(Endpoint.AppFetch, response.GetBytes()); + throw new PebbleException("The pebble requested the wrong UUID"); + } + + var putBytesResponse = await _pebble.PutBytesClient.PutBytes(bundle.App, TransferType.Binary, appInstallId:(uint)runStateResult.AppId); + if (!putBytesResponse) + { + throw new PebbleException("Putbytes failed"); + } + + if (bundle.HasResources) + { + putBytesResponse = await _pebble.PutBytesClient.PutBytes(bundle.Resources, TransferType.Resources, appInstallId:(uint)runStateResult.AppId); + if (!putBytesResponse) + { + throw new PebbleException("Putbytes failed"); + } + } + + //TODO: add worker to manifest and transfer it if necassary + //if (bundle.HasWorker) + //{ + //await PutBytesV3(bundle.Worker, TransferType.Worker, runStateResult.AppId); + //} + + } + else + { + throw new PebbleException("BlobDB Insert Failed"); + } + } + + private async Task InstallAppLegacyV2(AppBundle bundle, IProgress progress= null) + { + if ( bundle == null ) + throw new ArgumentNullException( "bundle" ); + + if ( progress != null ) + progress.Report( new ProgressValue( "Removing previous install(s) of the app if they exist", 1 ) ); + ApplicationMetadata metaData = bundle.AppMetadata; + UUID uuid = metaData.UUID; + + AppbankInstallResponse appbankInstallResponse = await RemoveAppByUUID( uuid ); + if ( appbankInstallResponse.Success == false ) + return; + + if ( progress != null ) + progress.Report( new ProgressValue( "Getting current apps", 20 ) ); + AppbankResponse appBankResult = await GetAppbankContentsAsync(); + + if ( appBankResult.Success == false ) + throw new PebbleException( "Could not obtain app list; try again" ); + AppBank appBank = appBankResult.AppBank; + + byte firstFreeIndex = 1; + foreach ( App app in appBank.Apps ) + if ( app.Index == firstFreeIndex ) + firstFreeIndex++; + if ( firstFreeIndex == appBank.Size ) + throw new PebbleException( "All app banks are full" ); + + if ( progress != null ) + progress.Report( new ProgressValue( "Transferring app to Pebble", 40 ) ); + + if ( await _pebble.PutBytesClient.PutBytes( bundle.App, TransferType.Binary,index:firstFreeIndex ) == false ) + throw new PebbleException( "Failed to send application binary pebble-app.bin" ); + + if ( bundle.HasResources ) + { + if ( progress != null ) + progress.Report( new ProgressValue( "Transferring app resources to Pebble", 60 ) ); + if ( await _pebble.PutBytesClient.PutBytes( bundle.Resources, TransferType.Resources,index:firstFreeIndex ) == false ) + throw new PebbleException( "Failed to send application resources app_resources.pbpack" ); + } + + if ( progress != null ) + progress.Report( new ProgressValue( "Adding app", 80 ) ); + await AddApp( firstFreeIndex ); + if ( progress != null ) + progress.Report( new ProgressValue( "Done", 100 ) ); + } + + + + public async Task InstallFirmwareAsync( FirmwareBundle bundle, IProgress progress = null ) + { + if ( bundle == null ) throw new ArgumentNullException( "bundle" ); + + if ( progress != null ) + progress.Report( new ProgressValue( "Starting firmware install", 1 ) ); + if ( ( await _pebble.SendSystemMessageAsync( SystemMessage.FirmwareStart ) ).Success == false ) + { + return false; + } + + if ( bundle.HasResources ) + { + if ( progress != null ) + progress.Report( new ProgressValue( "Transfering firmware resources", 25 ) ); + if ( await _pebble.PutBytesClient.PutBytes( bundle.Resources, TransferType.SysResources,index:0 ) == false ) + { + return false; + } + } + + if ( progress != null ) + progress.Report( new ProgressValue( "Transfering firmware", 50 ) ); + if ( await _pebble.PutBytesClient.PutBytes( bundle.Firmware, TransferType.Firmware,index:0 ) == false ) + { + return false; + } + + if ( progress != null ) + progress.Report( new ProgressValue( "Completing firmware install", 75 ) ); + bool success = ( await _pebble.SendSystemMessageAsync( SystemMessage.FirmwareComplete ) ).Success; + + if ( progress != null ) + progress.Report( new ProgressValue( "Done installing firmware", 100 ) ); + + return success; + } + + public async Task RemoveAppByUUID( UUID uuid ) + { + byte[] data = Util.CombineArrays( new byte[] { 2 }, uuid.Data ); + return await _pebble.SendMessageAsync( Endpoint.AppManager, data ); + } + + public async Task AddApp( byte index ) + { + byte[] data = Util.CombineArrays( new byte[] { 3 }, Util.GetBytes( (uint)index ) ); + await _pebble.SendMessageNoResponseAsync( Endpoint.AppManager, data ); + } + + public async Task GetAppbankContentsAsync() + { + return await _pebble.SendMessageAsync( Endpoint.AppManager, new byte[] { 1 } ); + } + } +} + diff --git a/PebbleSharp.Core/Pebble.cs b/PebbleSharp.Core/Pebble.cs index 178f387..5bf157e 100644 --- a/PebbleSharp.Core/Pebble.cs +++ b/PebbleSharp.Core/Pebble.cs @@ -3,9 +3,13 @@ using System.Diagnostics; using System.Globalization; using System.Linq; +using System.Text; using System.Threading.Tasks; using PebbleSharp.Core.Bundles; +using PebbleSharp.Core.AppMessage; using PebbleSharp.Core.Responses; +using PebbleSharp.Core.BlobDB; +using PebbleSharp.Core.Install; namespace PebbleSharp.Core { @@ -16,19 +20,15 @@ namespace PebbleSharp.Core /// public abstract class Pebble { - public enum SessionCaps : uint - { - GAMMA_RAY = 0x80000000 - } - - public const byte PEBBLE_CLIENT_VERSION = 2; - - private readonly PebbleProtocol _PebbleProt; + public FirmwareVersion Firmware { get; private set;} + private readonly PebbleProtocol _PebbleProt; private readonly Dictionary> _callbackHandlers; private readonly ResponseManager _responseManager = new ResponseManager(); - private uint _RemoteCaps = (uint)( RemoteCaps.Telephony | RemoteCaps.SMS | RemoteCaps.Android ); - private uint _SessionCaps = (uint)SessionCaps.GAMMA_RAY; + + public BlobDBClient BlobDBClient { get; private set;} + public PutBytesClient PutBytesClient { get; private set;} + public InstallClient InstallClient { get; private set;} /// /// Create a new Pebble @@ -40,15 +40,18 @@ public enum SessionCaps : uint /// protected Pebble( IBluetoothConnection connection, string pebbleId ) { - ResponseTimeout = TimeSpan.FromSeconds( 5 ); + ResponseTimeout = TimeSpan.FromSeconds( 5 ); PebbleID = pebbleId; + BlobDBClient = new BlobDBClient(this); + PutBytesClient = new PutBytesClient(this); + InstallClient = new InstallClient(this); _callbackHandlers = new Dictionary>(); _PebbleProt = new PebbleProtocol( connection ); _PebbleProt.RawMessageReceived += RawMessageReceived; - RegisterCallback( OnApplicationMessageReceived ); + RegisterCallback( OnApplicationMessageReceived ); } /// @@ -68,25 +71,6 @@ public IBluetoothConnection Connection public TimeSpan ResponseTimeout { get; set; } - /// - /// Set the capabilities you want to tell the Pebble about. - /// Should be called before connecting. - /// - /// - /// - public void SetCaps( uint? sessionCap = null, uint? remoteCaps = null ) - { - if ( sessionCap != null ) - { - _SessionCaps = (uint)sessionCap; - } - - if ( remoteCaps != null ) - { - _RemoteCaps = (uint)remoteCaps; - } - } - /// /// Connect with the Pebble. /// @@ -104,20 +88,21 @@ public async Task ConnectAsync() if ( response != null ) { - byte[] prefix = { PEBBLE_CLIENT_VERSION, 0xFF, 0xFF, 0xFF, 0xFF }; - byte[] session = Util.GetBytes( _SessionCaps ); - byte[] remote = Util.GetBytes( _RemoteCaps ); - - byte[] msg = Util.CombineArrays( prefix, session, remote ); - //\x01\xff\xff\xff\xff\x80\x00\x00\x00\x00\x00\x002 - await SendMessageNoResponseAsync( Endpoint.PhoneVersion, msg ); + var message = new AppVersionResponse(); + await SendMessageNoResponseAsync( Endpoint.PhoneVersion, message.GetBytes() ); IsAlive = true; + + //get the firmware details, we'll need to know the platform and version for possible future actions + var firmwareResponse = await this.GetFirmwareVersionAsync(); + this.Firmware = firmwareResponse.Firmware; } else { Disconnect(); } - } + } + + /// /// Disconnect from the Pebble, if a connection existed. @@ -247,93 +232,7 @@ public async Task BadPingAsync() return await SendMessageAsync( Endpoint.Ping, cookie ); } - public async Task InstallAppAsync( AppBundle bundle, IProgress progress = null ) - { - if ( bundle == null ) - throw new ArgumentNullException( "bundle" ); - - if ( progress != null ) - progress.Report( new ProgressValue( "Removing previous install(s) of the app if they exist", 1 ) ); - ApplicationMetadata metaData = bundle.AppMetadata; - UUID uuid = metaData.UUID; - - AppbankInstallResponse appbankInstallResponse = await RemoveAppByUUID( uuid ); - if ( appbankInstallResponse.Success == false ) - return; - - if ( progress != null ) - progress.Report( new ProgressValue( "Getting current apps", 20 ) ); - AppbankResponse appBankResult = await GetAppbankContentsAsync(); - - if ( appBankResult.Success == false ) - throw new PebbleException( "Could not obtain app list; try again" ); - AppBank appBank = appBankResult.AppBank; - - byte firstFreeIndex = 1; - foreach ( App app in appBank.Apps ) - if ( app.Index == firstFreeIndex ) - firstFreeIndex++; - if ( firstFreeIndex == appBank.Size ) - throw new PebbleException( "All app banks are full" ); - - if ( progress != null ) - progress.Report( new ProgressValue( "Transferring app to Pebble", 40 ) ); - - if ( await PutBytes( bundle.App, firstFreeIndex, TransferType.Binary ) == false ) - throw new PebbleException( "Failed to send application binary pebble-app.bin" ); - - if ( bundle.HasResources ) - { - if ( progress != null ) - progress.Report( new ProgressValue( "Transferring app resources to Pebble", 60 ) ); - if ( await PutBytes( bundle.Resources, firstFreeIndex, TransferType.Resources ) == false ) - throw new PebbleException( "Failed to send application resources app_resources.pbpack" ); - } - - if ( progress != null ) - progress.Report( new ProgressValue( "Adding app", 80 ) ); - await AddApp( firstFreeIndex ); - if ( progress != null ) - progress.Report( new ProgressValue( "Done", 100 ) ); - } - - public async Task InstallFirmwareAsync( FirmwareBundle bundle, IProgress progress = null ) - { - if ( bundle == null ) throw new ArgumentNullException( "bundle" ); - - if ( progress != null ) - progress.Report( new ProgressValue( "Starting firmware install", 1 ) ); - if ( ( await SendSystemMessageAsync( SystemMessage.FirmwareStart ) ).Success == false ) - { - return false; - } - - if ( bundle.HasResources ) - { - if ( progress != null ) - progress.Report( new ProgressValue( "Transfering firmware resources", 25 ) ); - if ( await PutBytes( bundle.Resources, 0, TransferType.SysResources ) == false ) - { - return false; - } - } - - if ( progress != null ) - progress.Report( new ProgressValue( "Transfering firmware", 50 ) ); - if ( await PutBytes( bundle.Firmware, 0, TransferType.Firmware ) == false ) - { - return false; - } - - if ( progress != null ) - progress.Report( new ProgressValue( "Completing firmware install", 75 ) ); - bool success = ( await SendSystemMessageAsync( SystemMessage.FirmwareComplete ) ).Success; - - if ( progress != null ) - progress.Report( new ProgressValue( "Done installing firmware", 100 ) ); - - return success; - } + public async Task GetFirmwareVersionAsync() { @@ -349,15 +248,6 @@ public async Task GetTimeAsync() return await SendMessageAsync( Endpoint.Time, new byte[] { 0 } ); } - /// - /// Fetch the contents of the Appbank. - /// - /// - public async Task GetAppbankContentsAsync() - { - return await SendMessageAsync( Endpoint.AppManager, new byte[] { 1 } ); - } - /// /// Remove an app from the Pebble, using an App instance retrieved from the Appbank. /// @@ -372,13 +262,13 @@ public async Task RemoveAppAsync( App app ) return await SendMessageAsync( Endpoint.AppManager, msg ); } - private async Task SendSystemMessageAsync( SystemMessage message ) + public async Task SendSystemMessageAsync( SystemMessage message ) { byte[] data = { 0, (byte)message }; return await SendMessageAsync( Endpoint.SystemMessage, data ); } - private async Task SendMessageAsync( Endpoint endpoint, byte[] payload ) + public async Task SendMessageAsync( Endpoint endpoint, byte[] payload ) where T : class, IResponse, new() { return await Task.Run( () => @@ -412,7 +302,7 @@ private async Task SendMessageAsync( Endpoint endpoint, byte[] payload ) } ); } - private Task SendMessageNoResponseAsync( Endpoint endpoint, byte[] payload ) + public Task SendMessageNoResponseAsync( Endpoint endpoint, byte[] payload ) { return Task.Run( () => { @@ -435,101 +325,74 @@ private Task SendMessageNoResponseAsync( Endpoint endpoint, byte[] payload ) private void RawMessageReceived( object sender, RawMessageReceivedEventArgs e ) { - Debug.WriteLine( "Received {0} message: {1}", (Endpoint)e.Endpoint, BitConverter.ToString( e.Payload ) ); + var endpoint = (Endpoint)e.Endpoint; + IResponse response = _responseManager.HandleResponse( endpoint, e.Payload ); - IResponse response = _responseManager.HandleResponse( (Endpoint)e.Endpoint, e.Payload ); + if (response != null) + { + if (e.Endpoint == (ushort)Endpoint.PhoneVersion) + { + var message = new AppVersionResponse(); + SendMessageNoResponseAsync(Endpoint.PhoneVersion, message.GetBytes()).Wait(); + } - if ( response != null ) - { - //Check for callbacks - List callbacks; - if ( _callbackHandlers.TryGetValue( response.GetType(), out callbacks ) ) - { - foreach ( CallbackContainer callback in callbacks ) - callback.Invoke( response ); - } - } - } + //Check for callbacks + List callbacks; + if (_callbackHandlers.TryGetValue(response.GetType(), out callbacks)) + { + foreach (CallbackContainer callback in callbacks) + callback.Invoke(response); + } + + } - private void OnApplicationMessageReceived( ApplicationMessageResponse response ) - { - SendMessageNoResponseAsync( Endpoint.ApplicationMessage, new byte[] { 0xFF, response.TID } ); } - public override string ToString() + public async Task SendBlobDBMessage(BlobDBCommandPacket command) + { + //TODO: I'm not sure we should assume that the first blobdb response we get is the one that + //corresponds to this request, we probably need to do extra work here to match up the token + var bytes = command.GetBytes(); + + return await SendMessageAsync(Endpoint.BlobDB,bytes ); + } + + public async Task SendApplicationMessage(AppMessagePacket data) { - return string.Format( "Pebble {0} on {1}", PebbleID, Connection ); + //DebugMessage(data.GetBytes()); + return await SendMessageAsync(Endpoint.ApplicationMessage, data.GetBytes()); } - private async Task RemoveAppByUUID( UUID uuid ) + //self._pebble.send_packet(AppRunState(data=AppRunStateStart(uuid=app_uuid))) + public async Task LaunchApp(UUID uuid) { - byte[] data = Util.CombineArrays( new byte[] { 2 }, uuid.Data ); - return await SendMessageAsync( Endpoint.AppManager, data ); + var data = new AppMessagePacket(); + data.ApplicationId = uuid; + data.Command = (byte)Command.Push; + data.TransactionId = 1; + data.Values.Add(new AppMessageUInt8() { Key=1,Value = 1 });//this one is key 0, doesn't actually do anything + + await SendMessageNoResponseAsync(Endpoint.Launcher, data.GetBytes()); } - private async Task PutBytes( byte[] binary, byte index, TransferType transferType ) + private void OnApplicationMessageReceived( AppMessagePacket response ) { - byte[] length = Util.GetBytes( binary.Length ); + SendMessageNoResponseAsync( Endpoint.ApplicationMessage, new byte[] { 0xFF, response.Values!=null ? response.TransactionId :(byte)0} ); + } - //Get token - byte[] header = Util.CombineArrays( new byte[] { 1 }, length, new[] { (byte)transferType, index } ); - - var rawMessageArgs = await SendMessageAsync( Endpoint.PutBytes, header ); - if ( rawMessageArgs.Success == false ) - return false; - - byte[] tokenResult = rawMessageArgs.Response; - byte[] token = tokenResult.Skip( 1 ).ToArray(); - - const int BUFFER_SIZE = 2000; - //Send at most 2000 bytes at a time - for ( int i = 0; i <= binary.Length / BUFFER_SIZE; i++ ) - { - byte[] data = binary.Skip( BUFFER_SIZE * i ).Take( BUFFER_SIZE ).ToArray(); - byte[] dataHeader = Util.CombineArrays( new byte[] { 2 }, token, Util.GetBytes( data.Length ) ); - var result = await SendMessageAsync( Endpoint.PutBytes, Util.CombineArrays( dataHeader, data ) ); - if ( result.Success == false ) - { - await AbortPutBytesAsync( token ); - return false; - } - } - - //Send commit message - uint crc = Crc32.Calculate( binary ); - byte[] crcBytes = Util.GetBytes( crc ); - byte[] commitMessage = Util.CombineArrays( new byte[] { 3 }, token, crcBytes ); - var commitResult = await SendMessageAsync( Endpoint.PutBytes, commitMessage ); - if ( commitResult.Success == false ) + private void DebugMessage(byte[] bytes) + { + StringBuilder payloadDebugger = new StringBuilder(); + foreach (var b in bytes) { - await AbortPutBytesAsync( token ); - return false; + payloadDebugger.Append(string.Format("{0}:", b)); } - - //Send complete message - byte[] completeMessage = Util.CombineArrays( new byte[] { 5 }, token ); - var completeResult = await SendMessageAsync( Endpoint.PutBytes, completeMessage ); - if ( completeResult.Success == false ) - { - await AbortPutBytesAsync( token ); - } - return completeResult.Success; } - private async Task AbortPutBytesAsync( byte[] token ) - { - if ( token == null ) throw new ArgumentNullException( "token" ); - - byte[] data = Util.CombineArrays( new byte[] { 4 }, token ); - - return await SendMessageAsync( Endpoint.PutBytes, data ); - } - - private async Task AddApp( byte index ) + public override string ToString() { - byte[] data = Util.CombineArrays( new byte[] { 3 }, Util.GetBytes( (uint)index ) ); - await SendMessageNoResponseAsync( Endpoint.AppManager, data ); + return string.Format( "Pebble {0} on {1}", PebbleID, Connection ); } private class CallbackContainer @@ -557,25 +420,10 @@ public void Invoke( IResponse response ) } } - private enum TransferType : byte - { - Firmware = 1, - Recovery = 2, - SysResources = 3, - Resources = 4, - Binary = 5 - } + public void Reset(ResetCommand command) + { + _PebbleProt.SendMessage((ushort)Endpoint.Reset, new byte[] { (byte)command }); + } - private enum SystemMessage : byte - { - FirmwareAvailible = 0, - FirmwareStart = 1, - FirmwareComplete = 2, - FirmwareFail = 3, - FirmwareUpToDate = 4, - FirmwareOutOfDate = 5, - BluetoothStartDiscoverable = 6, - BluetoothEndDiscoverable = 7 - } } } \ No newline at end of file diff --git a/PebbleSharp.Core/PebbleSharp.Core.csproj b/PebbleSharp.Core/PebbleSharp.Core.csproj index e88aded..47dc1a4 100644 --- a/PebbleSharp.Core/PebbleSharp.Core.csproj +++ b/PebbleSharp.Core/PebbleSharp.Core.csproj @@ -36,6 +36,7 @@ + @@ -59,7 +60,7 @@ - + @@ -67,7 +68,6 @@ - @@ -76,6 +76,20 @@ + + + + + + + + + + + + + +