2022年11月25日金曜日

2022年11月24日木曜日

解の部分修正

 という動画をアップしました。



解の修正をスケジュールナースの外側で(Excel等)行って頂くことは、問題ないのですが、次の点に留意する必要があります。

■今月解が変わります。

来月勤務表における先月部を編集して実態と合わせる必要があります

■勤務品質が担保されません。

スケジュールナースの管理の外で、修正すると多くの場合解の品質は悪化します。少なくともスケジュールナース上の目的関数値は、悪化する可能性が高いと思われます。スケジュールナースの結果は、最適解もしくは、準最適解ですから、目的関数値最小の組み合わせを出力しています。実際に管理の外で入力した修正を予定制約上で入力して頂くと、悪化を確認できると思います。


上の問題を回避する方法が、上記動画による方法です。

■ポイントは、勤務希望が解で上書きされないようにロックすること

■手修正を補償するために、適当な箇所をブランクにすること

■ブランク範囲が狭いとハードエラーとなります。

やってみると分かりますが、箇所によっては、かなり広範囲にブランクにしないと目的関数値が悪化します。制約は、広範囲に影響し合っている、ということです。

ソフト制約の数がそれほど多くないときに、スケジュールナース外で行うことは、可能かもしれません。しかし、動画で取り上げている規模のソフト制約が入っていた場合、目的関数値を悪化させずに行うのは、人間技ではかなり難しいと思います。




2022年11月23日水曜日

covid-19 勤務表をリリースした後での最小変更

 第8波が急速に広がっており、私の知る介護施設でもクラスタが発生しました。

勤務表リリース後に、スタッフが感染すると2週間休むことになるので、多大な損失になります。しかも夜勤等を手当てしつつ、なおかつリリース済みの勤務表に対してなるべく予定変更が最小となるような勤務表を再リリースする必要があります。そのような場合のスケジュールナースの対応の仕方です。

<シナリオ>

次のリリース済み勤務表のど真ん中で、スタッフQがcovid-19に感染し月末まで休みになるとします。

<手順>

1)スタッフの勤務希望は、変えないものとします。

2)予定画面の全てにロックを掛けます

3)解を予定に送ります

4)ロックを掛けた部分は、上書きされません

5)スタッフQの15日~を休みに変更し、ロックします。

6)15日から~の勤務予定を全てソフト制約レベル7に変更します。

(ロック部は、そのままソフト制約・ハード制約に関わらずそのままの状態を維持し変更されません。)

7)スタッフプロパティで、Qに関わる制約をクリアにします。

8)予定制約レベル7の求解重みを変更します。

9)求解します。

10)重みや、回数偏差リミッタを調整する、カット&トライ、 9),10)の繰り返し を行う

するのがミソです。



Youtube https://youtu.be/U4Bg-8SnHP8



2022年11月22日火曜日

サブスクリプション試用と定期請求無効のやりかた

 「毎月のサブスクリプション」の試用期間が切れました。毎月のサブスクリプションの試用はもう出来ません。

スケジュールナースのサブスクリプションは二つあり毎月ではなく毎1年間バージョンもあります。今回は、1年間のバージョンを購入します。すぐに、定期請求を無効として、実際には課金されないようにする例を示します。

前回ブログの毎月の試用期間が切れたので、下のダイアログが出ます。(下に隠れている場合もあります)はい、をクリックします。

いいえ、をクリックすると1年間になります。
ここからは、スケジュールナースではなく、Microsoft APIが行っています。マイクロソフトのアカウントメールアドレスとパスワードが求められます。
セカンドの支払い方法もAmazonで買ったXboxプロペイドカードを試みたのですが、
マイクロソフトアカウントに取り込まれるだけで、バックアップの支払い方法を求められるのは変わりませんでした。結局クレジットカードを選択しました。








次へをクリックします。
申し込むをクリックします。
サブスクリプション購入ありがとうございました、と出力されて使用可能になります。このダイアログは、スケジュールナースが出しています。

<課金されないようにする>

定期請求を無効にする処理です。確実に課金されないようにするには、有効期限の2日前までに次の処理を行います。


マイクロソフトアカウントに入ります。サブスクリプション1年が登録されています。

管理をクリックします。



定期請求無効をクリックします。


有効期限までは、試用期間でありその間は試用可能です。
まとめ
■試用するには、サブスクリプションを購入する必要があります。
■サブスクリプション購入は、プリペードだけでは出来ないようです。(クレジットカードが必要)
■試用期間は、1週間あります。有効期限前2日前までに定期請求をオフとすることで課金されません。
■スケジュールナースのサブスクリプションは二つのタイプ(月毎、年毎)があり、各々試用期間があります。どちらも試用することで計2週間、無料試用することができます。ただし、課金されないように、各々無効処理することが必要となります。



2022年11月17日木曜日

特許年金支払い

 特許権の維持は、登録年金として1年度から3年度まで支払い済みです。

特許登録番号が判明しないと、4年度目からの年金を納めることができません。私は、とりあえず、登録証が届いたら10年度まで7年分納めることにしています。

小規模事業者なので、定額の1/3程度で特許権の維持が可能です。11年度以降、維持特許料が一挙、数倍に上がるので、難しいのですが、10年までは、かなり少なく維持することが可能となっています。

2022年11月16日水曜日

特許証が届きました。

 新たに出願した特許が、菅原システムズ4件目の特許登録となりました。

菅原システムズの特許査定率(出願に対する特許査定率)は100%です。

<技術解説>

そのいずれも、ナーススケジューリング問題に対するソルバに関する特許です。ナーススケジューリングは、組み合わせ最適化における一分野です。最適化は、連続系最適化問題と離散系(整数系)最適化問題に分けられます。組み合わせ最適化は、整数最適化問題です。連続系最適化なら、例えば人を3.6人配置、という解が出てくるかもしれないのですが、欲しい解は、人をぶった切ることではありません。解は、全て整数である必要があります。そうなると解の様相は、連続系のそれとは、大きく異なる可能性があります。離散系であるグラフ処理と連続系Simplexをハイブリッドに活用することです。



問題を解く核になるエンジンのことをソルバと言います。

実は、ソルバそのものの特許を持つ企業は、国内ではほとんどありません。ソルバの使い方とか、ソルバを使ったアプリの類は、多くの企業が特許出願していますが、核となる問題解決ソルバは、技術深度が極めて深く、日本での出願も少ないのが現状です。

今回の特許は、ここ2年位、数々の国際ナーススケジューリングベンチマーク群の記録更新の元となった技術です。数理ソルバと呼ばれる類の分野の技術で、学術的にはオペレーションリサーチに属します。どちらかと言えば、ベンチマークに強いです。実務的にも、下界が判明するので、最適解かどうか疑われる向きには役に立つ場面があると思います。さらに強みは、マルチコア下でより力を発揮するので、時代が進めば進むほど、より輝く技術となっております。

ちなみに前回の取得特許は、2017年で、原因分析に関する技術です。解がないときのReasoningに関する技術で、学術的には、人口知能(AI)に属します。地味ではあるけれども、前回特許も実務には欠かせない技術です。

オペレーションリサーチとAIの融合は、永続的研究テーマです。前回特許以来、開発に5年を要しましたが、私なりの一つの回答を示せたと思います。

菅原システムズは、ナーススケジューリング問題では、国内トップ、国際的にみても有数の知的所有権保持者であると言えます。

最適化ソルバの特許は、殆どがCplex/Gurobi/Mosek等、米国発だったのですが、最近の傾向として中国系企業が目立ってきました。基本特許力は、国力に比例すると思うのは、私だけでしょうか?





2022年11月15日火曜日

池上先生が退官

 シンポジウム 「モデリングを通して見えた世界」 (ikegami-lab.tokyo)

池上先生の過去の論文を検索している途中、遅ればせながら知りました。

講演の先生方も、皆存じ上げている先生方です。

池上先生の研究室にお邪魔したことがあるのですが、ネットワーク表現のお話をお聞きしました。

私のようなアカデミック外の者を、最も権威のある学会に招待していただいたりして、当時色々反発はあったのではないかと、今にして思います。

池上先生が築かれたナーススケジューリングの礎は、きっと日本で発展するものと信じています。


日本のシフトはスケジュールナースが必ず良くします。





2022年11月14日月曜日

2025年度問題

目前に迫る2025年問題とは? 何が起き、どう備えるべきかを徹底解説 | 記事・トピックス一覧 | 法人のお客さま | PERSOL(パーソル)グループ (persol-group.co.jp)

総務省によると、団塊の世代は各年齢200万人。つまり毎年後期高齢者は200万人ずつ増えていく計算で、2025年には2,180万人ほどになると予測されます。日本人の4人に1人が75歳以上という未来があと3年後に迫っていることになります。

シフトで働く全ての方々の為に、スケジュールナースを用いてシフトの品質を改善することを提案しています。


2022年11月13日日曜日

サブスクリプション βテスター募集

 サブスクリプションのβテスターを募集します。募集完了しました

テスターの条件は、

■マイクロソフトアカウントがあること

■マイクロソフトアカウントにクレジットカードが登録されていること

■サブスクリプションのテストにご参加頂き2ヶ月分(サブスクリプション2ヶ月分@480x2)をご負担頂けること

です。


<お礼>

テスト後(2ヶ月)にサブスクリプションをキャンセルして頂きます。

代わりに、買い切りライセンス(永続ライセンス@50000相当)をプレゼントします。

参加頂ける方はサポートまでご連絡ください。(定員となり次第、締め切りします。)

下記は、サブスクリプション購入の様子です。実際に以下のような感じで操作しサブスクリプションを購入して頂きます。

現在は、公開されていませんが、βテスターには、URLをご案内します。

入手をクリックします。ダウンロードが始まります。2-3分かかるようです。



開くをクリックします。
サブスクリプションの説明が表示されます。この表示はマイクロソフトアカウントに
接続されていれば、一回だけ表示され、以降は表示されないようです。(ダイアログを出力しているのはスケジュールナースですが..)
とりあえず、いいえをクリックします。

購入しなかったので、次の表示となります。終了します。


再度、起動します。今度は「はい」をクリックします。
サブスクリプションは、1ヶ月毎と1年毎があります。「はい」で一ヶ月を選択します。
ここからは、スケジュールナースではなくストアAPIが出力しているダイアログになります。マイクロソフトアカウント名を入力します。
アカウントのパスワードを入力します。
次へをクリックします。
私の場合、Amazonでプレペイド@1500を購入しており、それを使いたかったのですが、下の表示では、切れた場合の処置が必須となっており、結局クレジットカードが必要となるようです。

申し込むをクリックします。
申し込むをクリックします。
成功すると「ご購入ありがとうございます。」が表示されます。これは、スケジュールナースがストアAPIの結果より出力しています。
いったん終了し再起動します。
通常は、ここより起動します。
以上が、サブスクリプション購入に関する手続きです。
購入すると、マイクロソフトよりアカウントメールアドレスに以下のようなメールが届きます。
****
サブスクリプション毎月 の無料試用版へようこそ

サブスクリプション毎月 をお試しいただき、ありがとうございます。ご利用に感謝いたします。
サブスクリプション毎月 の試用版は Sunday, November 13, 2022 から開始し、8 日間無料です。
Sunday, November 20, 2022 以降、1 月 ごとに Microsoft account balance 宛で JPY 480.00 税金を含む を 課金されます。
この価格に変更がある場合は、事前にお知らせいたします。
....
****

<課金される前に無効にする処理>
試行期間だけで、止める場合の処理です。

マイクロソフトアカウント上で、
「定期請求を無効にする」をクリックします。

確認のダイアログが出現します。
選択すると、次のようなダイアログとなります。
定期請求オフ後、試行期間が残っていれば、操作は、試用期間一杯まで可能です。
試用期間が過ぎた後起動すると、サブスクリプション購入を促すダイアログが出現します。


いいえ、の場合次のダイアログが出て終了となってしまいます。






以下は、上のサブスクリプションに関わるスケジュールナースのC#コードです。
	{
            //GetContext();
            Debug.Print("Hello");
           context = StoreContext.GetDefault();
            IInitializeWithWindow initWindow = (IInitializeWithWindow)(object)context;
            initWindow.Initialize(System.Diagnostics.Process.GetCurrentProcess().MainWindowHandle);
            //context.OfflineLicensesChanged += context_OfflineLicensesChanged;
            //Task task=SetupSubscriptionInfoAsync();//タスク引き取り待ちで抜ける
            //AsyncMethod();
            CheckLicenseState();
            //SetupSubscriptionInfoAsync();
            //if (!has_license)
            //{
            //    this.Close();
            //}
}
      private StoreContext context = null;
        private StoreProduct subscriptionStoreProductMonth;
        private StoreProduct subscriptionStoreProductYear;
        private StoreAppLicense license;
#if STORE
        private bool has_license = false;//NOV032022
#endif
        private async Task PurchaseNonSubscLicense()
        {
            // Get app store product details
            StoreProductResult productResult = await context.GetStoreProductForCurrentAppAsync();
            if (productResult.ExtendedError != null)
            {
                // The user may be offline or there might be some other server failure
                //Debug.Print($"ExtendedError: {productResult.ExtendedError.Message}", NotifyType.ErrorMessage);
                return;
            }

            //rootPage.NotifyUser("Buying the full license...", NotifyType.StatusMessage);
            //StoreAppLicense license = await storeContext.GetAppLicenseAsync();
            //if (license.IsTrial)
            {
                StorePurchaseResult result = await productResult.Product.RequestPurchaseAsync();
                if (result.ExtendedError != null)
                {
                    //rootPage.NotifyUser($"Purchase failed: ExtendedError: {result.ExtendedError.Message}", NotifyType.ErrorMessage);
                    return;
                }

                switch (result.Status)
                {
                    case StorePurchaseStatus.AlreadyPurchased:
                        has_license = true;
                        Debug.Print("StorePurchaseStatus.AlreadyPurchased\n");
                        //rootPage.NotifyUser($"You already bought this app and have a fully-licensed version.", NotifyType.ErrorMessage);
                        break;

                    case StorePurchaseStatus.Succeeded:
                        Debug.Print("StorePurchaseStatus.Succeeded\n");
                        has_license = true;
                        // License will refresh automatically using the StoreContext.OfflineLicensesChanged event
                        break;

                    case StorePurchaseStatus.NotPurchased:
                        Debug.Print("StorePurchaseStatus.NotPurchased\n");
                        //rootPage.NotifyUser("Product was not purchased, it may have been canceled.", NotifyType.ErrorMessage);
                        break;

                    case StorePurchaseStatus.NetworkError:
                        Debug.Print("StorePurchaseStatus.NetworkError\n");
                        //rootPage.NotifyUser("Product was not purchased due to a Network Error.", NotifyType.ErrorMessage);
                        break;

                    case StorePurchaseStatus.ServerError:
                        Debug.Print("StorePurchaseStatus.ServerError\n");
                        //rootPage.NotifyUser("Product was not purchased due to a Server Error.", NotifyType.ErrorMessage);
                        break;

                    default:
                        Debug.Print("StorePurchaseStatus.default\n");
                        //rootPage.NotifyUser("Product was not purchased due to an Unknown Error.", NotifyType.ErrorMessage);
                        break;
                }
            }
            
        }
    



    private async Task PromptUserToPurchaseAsync(StoreProduct product)
        {
            // Request a purchase of the subscription product. If a trial is available it will be offered 
            // to the customer. Otherwise, the non-trial SKU will be offered.
            Debug.Print("RequestPurchaseAsync");
            StorePurchaseResult result = await product.RequestPurchaseAsync();

            // Capture the error message for the operation, if any.
            string extendedError = string.Empty;
            if (result.ExtendedError != null)
            {
                extendedError = result.ExtendedError.Message;
            }
            string stm = extendedError+" ";
            switch (result.Status)
            {
                case StorePurchaseStatus.Succeeded:
                    // Show a UI to acknowledge that the customer has purchased your subscription 
                    // and unlock the features of the subscription. 
                    has_license = true;
                    stm += rm.GetString("thank_you");

                    break;

                case StorePurchaseStatus.NotPurchased:
                    if (!has_license)
                    {
                        stm += rm.GetString("see_you_again");
                    }
                    has_license = false;
                    
                    System.Diagnostics.Debug.WriteLine("The purchase did not complete. " +
                        "The customer may have cancelled the purchase. ExtendedError: " + extendedError);
                    break;

                case StorePurchaseStatus.ServerError:
                case StorePurchaseStatus.NetworkError:
                    if (!has_license)
                    {
                        has_license = false;
                    }
                    stm += rm.GetString("server_error");
                    System.Diagnostics.Debug.WriteLine("The purchase was unsuccessful due to a server or network error. " +
                        "ExtendedError: " + extendedError);
                    break;

                case StorePurchaseStatus.AlreadyPurchased:
                    has_license = true;
                    stm += rm.GetString("already_puchased");
                    System.Diagnostics.Debug.WriteLine("The customer already owns this subscription." +
                            "ExtendedError: " + extendedError);
                    break;
            }
            MessageBox.Show(stm);

        }
        public string get_date_info(StoreDurationUnit u, uint billingPeriod)
        {
            string s = "";
            s += billingPeriod.ToString();
            switch (u)
            {
                case (StoreDurationUnit.Minute): s += rm.GetString("Minute");break;
                case (StoreDurationUnit.Hour): s += rm.GetString("Hour");break;
                case (StoreDurationUnit.Day): s += rm.GetString("Day");break;
                case (StoreDurationUnit.Week): s += rm.GetString("Weeks");break;
                case (StoreDurationUnit.Month): s += rm.GetString("Month");break;
                case (StoreDurationUnit.Year): s += rm.GetString("Year");break;
                
            }
            return s;
        }
        public async Task GetSubscriptionsInfo()
        {
            //if (context == null)
            //{
            //    context = StoreContext.GetDefault();
            //    // If your app is a desktop app that uses the Desktop Bridge, you
            //    // may need additional code to configure the StoreContext object.
            //    // For more info, see https://aka.ms/storecontext-for-desktop.
            //}

            // Subscription add-ons are Durable products.
            string[] productKinds = { "Durable" };
            List filterList = new List(productKinds);

            StoreProductQueryResult queryResult =
                await context.GetAssociatedStoreProductsAsync(productKinds);

            if (queryResult.ExtendedError != null)
            {
                // The user may be offline or there might be some other server failure.
                System.Diagnostics.Debug.WriteLine($"ExtendedError: {queryResult.ExtendedError.Message}");
                return null;
            }
            string stm = "";
            foreach (KeyValuePair item in queryResult.Products)
            {
                // Access the Store product info for the add-on.
               
                
                StoreProduct product = item.Value;
                //subscriptionStoreProduct = product;//productを保存する。

                string store_id = product.StoreId;
                string store_title = product.Title;
                string store_price = product.Price.FormattedPrice;

                stm += store_id +" ";
                stm += store_title + " ";
                stm += store_price + " ";
                // For each add-on, the subscription info is available in the SKU objects in the add-on. 
                foreach (StoreSku sku in product.Skus)
                {
                    if (sku.IsSubscription)
                    {
                        // Use the sku.SubscriptionInfo property to get info about the subscription. 
                        // For example, the following code gets the units and duration of the 
                        // subscription billing period.
                        StoreDurationUnit billingPeriodUnit = sku.SubscriptionInfo.BillingPeriodUnit;
                        if (billingPeriodUnit == StoreDurationUnit.Year)
                        {
                            subscriptionStoreProductYear = product;

                        }else if (billingPeriodUnit == StoreDurationUnit.Month)
                        {
                            subscriptionStoreProductMonth = product;
                        }else
                        {
                            Debug.Assert(false);
                        }
                        uint billingPeriod = sku.SubscriptionInfo.BillingPeriod;
                        stm += rm.GetString("subscription") + " ";
                        stm +=get_date_info(billingPeriodUnit,billingPeriod );
                    }
                    if (sku.IsTrial)
                    {
                        stm += rm.GetString("trial");
                        
                    }
                }
                stm += "\n";
            }
            Debug.Print(stm);
            //MessageBox.Show(stm);
            return queryResult;
        }
        private async Task CheckLicenseState()
        {
            //GetContext();
            Debug.Print("CheckLicenseState\n");
            StoreProductQueryResult sresult = await GetSubscriptionsInfo();
            if (subscriptionStoreProductYear==null && subscriptionStoreProductMonth==null)//sresult == null)//NonSubscription
            {
                Debug.Print("NonSubscription\n");
                StoreAppLicense appLicense = await context.GetAppLicenseAsync();
                if (appLicense != null)
                {
                    if (!appLicense.IsActive)
                    {
                        Debug.Print("!appLicense.IsActive\n");
                        await PurchaseNonSubscLicense();
                    }
                    else
                    {
                        Debug.Print("appLicense.IsActive\n");
                        has_license = true;
                    }
                }else
                {
                    Debug.Print("no license is active.\n");
                    
                }
            }else
            {
                Debug.Print("CheckSubscriptionAddonStatusesAsync\n");
                await CheckSubscriptionAddonStatusesAsync();
                if (!has_license)
                {
                    Debug.Print("purchase_subscription\n");
                    await purchase_subscription();
                }

            }
            if (!has_license)
            {
                MessageBox.Show(rm.GetString("no_license_detected"));
                this.Close();
            }
        }


        private async Task purchase_subscription()
        {

            //Dialog サブスクリプションを購入しますか
            //Yes/No
            //Dialog サブスクリプションは1か月と1年があります。1か月を購入しますか?
            //Yes/No/Cancel
            //   
            DialogResult result = MessageBox.Show(rm.GetString("purchase_subscription_q"), rm.GetString("purchase_subscription"),  MessageBoxButtons.YesNo);
            Debug.Print(rm.GetString("purchase_subscription_q"));


            if (result == System.Windows.Forms.DialogResult.Yes)
            {
                

                DialogResult result2 = MessageBox.Show(rm.GetString("purchase_subscription_which"), rm.GetString("purchase_subscription"), MessageBoxButtons.YesNoCancel);
                Debug.Print(rm.GetString("purchase_subscription_which"));
                if (result2 == System.Windows.Forms.DialogResult.Yes)
                {//一か月購入
                    Debug.Print("一か月購入");
                    await PromptUserToPurchaseAsync(subscriptionStoreProductMonth);
                }
                else if (result2 == System.Windows.Forms.DialogResult.No)
                {//一年購入
                    Debug.Print("一年購入");
                    await PromptUserToPurchaseAsync(subscriptionStoreProductYear);


                }
            }
        }
        public enum LicenseStatus
        {
            Unknown = 0,
            Trial,
            Full
        }
        public async Task CheckSubscriptionAddonStatusesAsync()
        {
            //_context ??= StoreContext.GetDefault();
            //GetContext();

            StoreProductQueryResult queryResult = await context.GetUserCollectionAsync(new[] { "Durable" });
            if (queryResult.ExtendedError != null)
            {
                MessageBox.Show(rm.GetString("trial_requires_subscription"));
                return;//ADDON はあって登録subscriptionはない
            }
            //    throw queryResult.ExtendedError;

            foreach (KeyValuePair pair in queryResult.Products)
            {
                StoreSku sku = pair.Value.Skus.FirstOrDefault();
                
                StoreCollectionData data = sku?.CollectionData;
                if (data != null)
                {
                    has_license = true;
                    
                    LicenseStatus status = data.IsTrial ? LicenseStatus.Trial : LicenseStatus.Full;
                    DateTimeOffset startDate = data.StartDate;
                    bool isAutoRenewal = IsAutoRenewal(data.ExtendedJsonData);

                    Debug.WriteLine($"Store ID ------- {pair.Key}");
                    Debug.WriteLine($"License status - {status}");
                    Debug.WriteLine($"Start date ----- {startDate:yyyy/MM/dd}");
                    Debug.WriteLine($"Auto renewal --- {isAutoRenewal}");
                }
            }
        }

        private  bool IsAutoRenewal(string json)
        {
            if (string.IsNullOrEmpty(json))
                return false;

            // For this JSON value, see:
            // https://docs.microsoft.com/en-us/windows/uwp/monetize/data-schemas-for-store-products
            var pattern = new Regex(@"""autoRenew"":(?true|false)", RegexOptions.IgnoreCase);
            var match = pattern.Match(json);
            return match.Success
                && bool.TryParse(match.Groups["value"].Value, out bool value)
                && value;
        }

2022年11月11日金曜日

Win32Sandbox

 Win32アプリ用のSandboxというのは存在しないのですが、Hyper-Vで実質的にその役割を果たす開発環境があります。下図で、quick creatをクリックするとWindows11の開発環境をHyper-V上に作ることができます。revertでチェックポイント時点の環境に戻せるので、何回でもソフトを初期インストールすることが出来ます。(ただし、microsoft アカウント自体は、初期化されないので、ソフトのライセンス初期化は出来ません。)  Windows11環境だとVisual Studio2022も内包されており、いたれりつくせりの開発環境が手に入ります。ただし20GB近くあるので、ダウンロードは、時間がかかります。





課金の通貨が、現在地域で変わるので、OSの言語を日本語環境に、現在地域を日本にします。

以下は、私の開発環境でのDebugの様子です。Visual Studioで、以下のようにデバッグ出来ます。








2022年11月10日木曜日

サービス分離

 サブスクだとクレジットカード払いがネックとなる方がいるので、サービスを以下のように分離しました。具体的には、

■プロジェクト作成

■ソフトウェア永続ライセンス

■ソフトウェア保守

として、分離しました

これを全部くっつけたものが旧サービスだったのですが、トータルすると前の方が安くなってしまいました。が、プロジェクト作成やメンテナンスは要らないから、永続ライセンスが安くならないか?というお問い合わせは頂いていたので、そういう方には朗報かと思います。

また、ソフトウェア保守というのも新設しました。過去、お客さまの中には、とても大きな変更を要求されてくるお客さまもいらっしゃいました。今までは、全て無償で対応させて頂いたのですが、そういう場合も作業実費は頂くという発想で、保守単位でお仕事をします、というスタイルに変更しました。契約1年間の私の総仕事時間を10時間Maxとして、その時間の範囲内でしたら、何回でもプロジェクト修正、仕様変更に応じるものです。最大で、宮城県の最低賃金に迫る時間単価となります。

<ストア提出パッケージ>

サブスクリプション用と永続ライセンスでストア提出パッケ-ジは同じバイナリとなります。アドオンの有無で判別してアプリ内で処理を分岐する実装としています。

<支払い先>

サブスクリプションの支払い先はマイクロソフトです。その他永続ライセンス等の支払い先は、菅原システムズになります。

<ソフトリリース元>

ソフトは、いずれもマイクロソフトストアよりのリリースとなります。

サブスクリプション用のストアURLは、公開します。サブスクションタイプは二つあり、

■一ヶ月毎

■一年毎

があります。サブスクリプションはいずれも一週間の試用期間があります。試用するにも、最初にサブスクのタイプを決定する必要があります。サブスクリプションの試用で注意することは、単に試用目的あるいはサブスクそのものを止める場合、確実に課金されないためには、有効期限の2日前にキャンセル処理をしないといけないことです。ストアアプリは、どれも同様なのですが、この辺、面倒なので別途解説します。(マイクロソフトにクレジットカードを登録したくない場合は、Amazonで定額のプリペイドカード(1000円、1500円..)を購入することでも可能です。)

永続ライセンス用URLは、公開しません。試用期間もありません。現ライセンスドユーザ、もしくは、新たに永続ライセンスを購入されるお客さまには、URL+プロモーションコードを発行します。これが、専用指定サイトとなり(ストア上では)無償インストールすることできます。(プロモーションコードがないと、無償にはなりませんし、プロモーションコードは一度しか作用しないので、仮に専用サイトURLが流出しても問題ありません。)バージョンアップも同様です。

未だ、ストアAPIを試行錯誤している段階なので、若干の変更があるかもしれませんが、骨格は、上記の通りです。

12月には、現ライセンスドユーザさま方々にストア専用サイトをご案内する予定です。マイクロソフトアカウントをご用意ください。PCが買い替えになっても、(私が保証できる訳ではありませんが)同じマイクロソフトアカウントであれば、無償インストールが可能の筈です。これで、Digital著名問題、Digital著名期限問題から逃れられますし、永続的にソフトは生き続けると思います。(保証ではありません。)


マイクロソフトアカウント

 今まで、私も、ローカルアカウントとマイクロソフトアカウントで違いを意識していなかったのですが、ストアアプリでは、必然的に意識する必要があります。開発者としては当然のことなのですが、ユーザも、意識する必要があります。

マイクロソフトアカウントとは?

【初心者向け】マイクロソフトアカウントとは? 初心者なら使わなくてもOK (note-pc.biz)

マイクロソフトストア支払い方法

マイクロソフトストアで支払い方法を変更する方法/支払いできない時の対処法|gatiho.com (nec-computers.com)



■アカウントが同じならば、他のPCでも総計10台まで使用できます。

■アカウントが違うと、基本的には使えません。

というところが違います。

現ライセンスドユーザにも同じストア版に移行していただく必要があります。ライセンスドユーザとサブスクユーザは、基本的に同じバイナリで、バージョンも同じく上がるようにしたいと思います。

勿論、その際、

■無償を維持

■プロジェクトファイルの互換を維持

する必要がありますが、その仕組みについては、別途説明します。




2022年11月9日水曜日

アプリ認定キット

 Win32でも行うべきのようです。キットは、appcertui.exeのようです。以下にありました。

これを起動して、パッケージを読み込ませると、
合格か不合格かを表示してくれます。とりあえず、合格となったパッケージがストア提出でNGとなったケースは一度もありません。なので、通常のWIN32でも合格出来れば、ストア提出でも問題なく通過出来るのではないかと思います。大体は、数時間
内に反映されているような感じです。


<appxupload ファイル作成>

次のようにファイルを選択してzipファイルを生成し、拡張子をappxuploadにすれば、



アップロードファイルが出来上がります。


2022年11月2日水曜日

WIN32ストアサブスクリプションコード

 C#でSTOREのサブスクリプションコードを書きました。サブスクリプションコードは、アプリ内にあります。一方サブスクリプションは、アドオンによる実装なので、どんな風にサブスクリプションのダイアログが表示されるのかストアに公開しないと分かりません。また、動くかどうか分からないないので、とりあえず、デバッグ用のユーザグループをPrivateで作成、公開してデバッグしようという戦略です。ユーザは、一度でも購入すると後戻り出来ないので、複数のPrivateユーザを設定しています。(0円設定なのでデバッグでお金はかかりません。従いα、β..とアプリの名前を変えつつRefineしていくしか方法がないように思います。Sandboxとかないようです。)

<C#からWindows RTを呼び出すコードのエラー>

Win32アプリであるC#からの呼び出し部でコンパイルエラーが生じました。


Error CS4036 'IAsyncOperation' does not contain a definition for 'GetAwaiter' and no extension method 'GetAwaiter' accepting a first argument of type 'IAsyncOperation' could be found (are you missing a using directive for 'System'?)


<対処方法>
https://stackoverflow.com/questions/61263356/how-do-i-get-an-app-licence-from-microsoft-store
https://stackoverflow.com/questions/44099401/frombluetoothaddressasync-iasyncoperation-does-not-contain-a-definition-for-get/44102996#44102996

nuget UwpDesktop-Updatedでインストール 症状同じ
nuget System.Runtime.WindowsRuntimeでインストール エラー解消




#if STORE
using Windows.Services.Store;

#endif

#if STORE
        async Task AsyncMethod()
        {

            await SetupSubscriptionInfoAsync();


        }
        private StoreContext context = null;
        private StoreProduct subscriptionStoreProduct;
        private bool has_lincese = false;
        public async Task SetupSubscriptionInfoAsync()
        {
            if (context == null)
            {
                context = StoreContext.GetDefault();
                // If your app is a desktop app that uses the Desktop Bridge, you
                // may need additional code to configure the StoreContext object.
                // For more info, see https://aka.ms/storecontext-for-desktop.
            }
            

            bool userOwnsSubscription = await CheckIfUserHasSubscriptionAsync();
            if (userOwnsSubscription)
            {

                // Unlock all the subscription add-on features here.
                has_lincese = true;
                return;
            }
            StoreProductQueryResult result= await GetSubscriptionsInfo();
            if (result == null)//subscriptionそのものがない
            {
                has_lincese = true;
                return;
            }
            // Prompt the customer to purchase the subscription.
            await PromptUserToPurchaseAsync();

          

        }

        private async Task CheckIfUserHasSubscriptionAsync()
        {
            StoreAppLicense appLicense = await context.GetAppLicenseAsync();

            // Check if the customer has the rights to the subscription.
            foreach (var addOnLicense in appLicense.AddOnLicenses)
            {
                StoreLicense license = addOnLicense.Value;
     
                {
                    if (license.IsActive)
                    {
                        DateTimeOffset now = DateTimeOffset.Now;
                        TimeSpan TS = now - license.ExpirationDate;
                        TimeSpan TSR = new TimeSpan(25, 0, 0, 0);//25days hours min sec
                        if (TS <=TSR)//25日以下になったら表示
                        {
                            string stm = rm.GetString("expiration_date") +  license.ExpirationDate.ToString();
                            MessageBox.Show(stm);
                        }
                        // The expiration date is available in the license.ExpirationDate property.
                        return true;
                    }
                }
            }

            // The customer does not have a license to the subscription.
            return false;
        }

        
        private async Task PromptUserToPurchaseAsync()
        {
            // Request a purchase of the subscription product. If a trial is available it will be offered 
            // to the customer. Otherwise, the non-trial SKU will be offered.
            StorePurchaseResult result = await subscriptionStoreProduct.RequestPurchaseAsync();

            // Capture the error message for the operation, if any.
            string extendedError = string.Empty;
            if (result.ExtendedError != null)
            {
                extendedError = result.ExtendedError.Message;
            }
            string stm = extendedError+" ";
            switch (result.Status)
            {
                case StorePurchaseStatus.Succeeded:
                    // Show a UI to acknowledge that the customer has purchased your subscription 
                    // and unlock the features of the subscription. 
                    has_lincese = true;
                    stm += rm.GetString("thank_you");

                    break;

                case StorePurchaseStatus.NotPurchased:
                    has_lincese = false;
                    stm += rm.GetString("see_you_again");
                    System.Diagnostics.Debug.WriteLine("The purchase did not complete. " +
                        "The customer may have cancelled the purchase. ExtendedError: " + extendedError);
                    break;

                case StorePurchaseStatus.ServerError:
                case StorePurchaseStatus.NetworkError:
                    has_lincese = false;
                    stm += rm.GetString("server_error");
                    System.Diagnostics.Debug.WriteLine("The purchase was unsuccessful due to a server or network error. " +
                        "ExtendedError: " + extendedError);
                    break;

                case StorePurchaseStatus.AlreadyPurchased:
                    has_lincese = true;
                    stm += rm.GetString("already_puchased");
                    System.Diagnostics.Debug.WriteLine("The customer already owns this subscription." +
                            "ExtendedError: " + extendedError);
                    break;
            }
            MessageBox.Show(stm);

        }
        public string get_date_info(StoreDurationUnit u, uint billingPeriod)
        {
            string s = "";
            s += billingPeriod.ToString();
            switch (u)
            {
                case (StoreDurationUnit.Minute): s += rm.GetString("Minute");break;
                case (StoreDurationUnit.Hour): s += rm.GetString("Hour");break;
                case (StoreDurationUnit.Day): s += rm.GetString("Day");break;
                case (StoreDurationUnit.Week): s += rm.GetString("Weeks");break;
                case (StoreDurationUnit.Month): s += rm.GetString("Month");break;
                case (StoreDurationUnit.Year): s += rm.GetString("Year");break;
                
            }
            return s;
        }
        public async Task GetSubscriptionsInfo()
        {
            if (context == null)
            {
                context = StoreContext.GetDefault();
                // If your app is a desktop app that uses the Desktop Bridge, you
                // may need additional code to configure the StoreContext object.
                // For more info, see https://aka.ms/storecontext-for-desktop.
            }

            // Subscription add-ons are Durable products.
            string[] productKinds = { "Durable" };
            List filterList = new List(productKinds);

            StoreProductQueryResult queryResult =
                await context.GetAssociatedStoreProductsAsync(productKinds);

            if (queryResult.ExtendedError != null)
            {
                // The user may be offline or there might be some other server failure.
                System.Diagnostics.Debug.WriteLine($"ExtendedError: {queryResult.ExtendedError.Message}");
                return null;
            }
            string stm = "";
            foreach (KeyValuePair item in queryResult.Products)
            {
                // Access the Store product info for the add-on.
               
                
                StoreProduct product = item.Value;
                subscriptionStoreProduct = product;//productを保存する。

                string store_id = product.StoreId;
                string store_title = product.Title;
                string store_price = product.Price.FormattedPrice;

                stm += store_id +" ";
                stm += store_title + " ";
                stm += store_price + " ";
                // For each add-on, the subscription info is available in the SKU objects in the add-on. 
                foreach (StoreSku sku in product.Skus)
                {
                    if (sku.IsSubscription)
                    {
                        // Use the sku.SubscriptionInfo property to get info about the subscription. 
                        // For example, the following code gets the units and duration of the 
                        // subscription billing period.
                        StoreDurationUnit billingPeriodUnit = sku.SubscriptionInfo.BillingPeriodUnit;

                        uint billingPeriod = sku.SubscriptionInfo.BillingPeriod;
                        stm += rm.GetString("subscription") + " ";
                        stm +=get_date_info(billingPeriodUnit,billingPeriod );
                    }
                    else if (sku.IsTrial)
                    {
                        stm += rm.GetString("trial");
                        
                    }
                }
                stm += "\n";
            }
            MessageBox.Show(stm);
            return queryResult;
        }
#endif