Prism

2025年10月20日 星期一

由於方格紙網站廣告收入微薄,  一直以來都架設在免費的GCP上服務, 目前GCP已不再提供免費架站, 且網站函式庫需使用PHP7, 已不符目前PHP8為主流的其它免費架站環境, 雖然不捨, 還是決定停止運作...

2020年8月28日 星期五

小米手環5 - 02 錶盤製作

   < 本文請勿轉載,謝謝 ! >

做了一些小米手環5 的錶盤,將作的錶盤做成一篇比較好整理,修改部份請仍參考 前一篇文章

目前做的錶盤如下網頁連結,有興趣的話可以下載用用看,AmazFaces 在 Google Play 和 Apple Store 都有APP,安裝後請搜尋 MyGraphPaper 可在手機查看錶盤:
全部錶盤 https://amazfitwatchfaces.com/search/mi-band-5/authorID/324908

1. 大數字:為了有點老花眼的自己做了一個大數字的錶盤
PS:電量指示 綠色 100% ~ 91% ,灰色 90% ~ 51%,黃色 50% ~ 31%,紅色 30%以下
AmazFaces 下載 Big&Simple - https://amazfitwatchfaces.com/mi-band-5/view/295

2. 窗戶:下載後可以參考前篇文章換成自己喜歡的圖片
AmazFaces 下載 The Windows - https://amazfitwatchfaces.com/mi-band-5/view/377

3. 出發:看若步數點亮如跑道的燈光,有著成就也有美麗...
AmazFaces 下載 Set off - https://amazfitwatchfaces.com/mi-band-5/view/390

4. 步數拼圖:實驗性作品...
AmazFaces 下載 Jigsaw Puzzle - AERITH - https://amazfitwatchfaces.com/mi-band-5/view/406

5. 挑戰迷宮:你能走出迷宮嗎?
AmazFaces 下載 Challenge the maze - https://amazfitwatchfaces.com/mi-band-5/view/411

6. 紙娃娃 - 智美
AmazFaces 下載 Paper doll - TOMOMI - https://amazfitwatchfaces.com/mi-band-5/view/473

7. 紙娃娃 - 公主
AmazFaces 下載 Every girl is a Princess ! - https://amazfitwatchfaces.com/mi-band-5/view/474

8. 運動中用錶盤
AmazFaces 下載 Sport  - https://amazfitwatchfaces.com/mi-band-5/view/498

9. 步數拼圖 FFVII remake:之前實驗性拼圖的實用化...
AmazFaces 下載 Jigsaw Puzzle - FFVII remake - https://amazfitwatchfaces.com/mi-band-5/view/546

10. 心率區間
AmazFaces 下載 Heart rate Zone - https://amazfitwatchfaces.com/mi-band-5/view/554

 


11. 街道:看時間也隨時留意一下自己的身體心率狀況
AmazFaces 下載 Street - https://amazfitwatchfaces.com/mi-band-5/view/677



2020.9.17 更新:

新版本的錶盤製作程式 MiBandWFTool_2.1.6 已能生成錶盤隨機 ID,AmazFaces 上傳至小米手環5的錶盤不會再互相覆蓋,下述方法不需再用了,已經使用新版程式更新站內所有錶盤,以後切換下載的錶盤方便很多。

在小米手環5的"更多->設定"中啟用"長按錶盤",就可長按螢幕快速切換錶盤。


多個 AmazFaces 錶盤共存

做了這麼多錶盤,但 AmazFaces 只能留一個錶盤在手環內,好不方便...

小米手環5 內可容納上傳 3 個自訂錶盤,但 AmazFaces下載的錶盤由於皆經網站提供的程式 ( MiBandWFTool_2.1.4 ) 所產生,每個錶盤的 ID 都是相同的,造成小米手環5 內就算有空間容納新錶盤,新上傳的錶盤還是因 ID 相同會蓋掉舊的 AmazFaces 錶盤,試了好久還好找到更改錶盤 ID 的方法,讓小米手環5 可容納多個 AmazFaces 錶盤,切換上方便很多。

1. 電腦請先至下列網站安裝 檔案修改軟體 HxD  Version 2.4.0.0
    https://mh-nexus.de/en/hxd/

2. 請先在小米運動APP內安裝上傳一個官方錶盤至手環,如 竹林熊貓,參考 上篇 進入錶盤目錄

"本機\(手機名稱)\內部儲存空間\Android\data\com.xiaomi.hm.health\files\watch_skin_file\58\edfde78987046f3738a8b562acb5731d"

將其 edfde78987046f3738a8b562acb5731d.bin 檔案下載至電腦中。


3.  打開HxD,將上之bin檔案拖放至HxD中,再複製如下圖反白區域的16進位資料


4. 同樣用 HxD 打開 AmazFaces 下載的錶盤,將上的反白區域資料貼上覆蓋 AmazFaces Bin 檔同樣的位置後存檔即可。(請注意,修改後的檔案大小是不變的)

這樣修改後的 AmazFaces 錶盤 ID 就是 竹林熊貓 的 ID,上傳至手環內就不會蓋掉其它 AmazFaces 錶盤,也不會被其它 AmazFaces 錶盤蓋掉(前提手環內要至少要保留 1個可上傳的空間),如法泡製至其它錶盤,就可在手環內保留多個 AmazFaces 錶盤。


找到喜歡的錶盤,自己動手換上 朋友、小孩、寵物、動漫...等照片,看時間感覺也會不同喔,祝大家身體健康!


2020年8月14日 星期五

小米手環5 - 01 錶盤修改

< 本文請勿轉載,謝謝 ! >

小米運動APP中 小米手環5 內建的錶盤不少,網上還有第三方提供的錶盤市場,加起來也有上百個可選,還再持續增加中,真的可以找到喜歡的錶盤,但有的錶盤想改 一週的語系、日期中日和月的前後順序、背景、人物更換...等等,讓錶盤更接近自己的喜好和習慣,看了錶盤市場AmazFaces討論區的說明,可以簡單的自己動手更改錶盤或自製錶盤...

錶盤市場 AmazFaces - https://amazfitwatchfaces.com/mi-band-5/fresh


一、下載 解封、封裝錶盤程式

請至下方連結 AmazFaces 討論區下載 解封、封裝錶盤程式  MiBandWFTool_2.1.6.zip,將其解壓縮在電腦目錄下即可 (討論區中有說到,解壓後若防毒軟體警告 WFPreview.exe 有木馬,可將其刪除,留下其它檔案即可)
https://amazfitwatchfaces.com/forum/viewtopic.php?f=29&t=720


二、解封錶盤

這邊找一個簡單的錶盤作說明:
1. 手機選擇小米運動APP中的錶盤 "交錯空間",並將"交錯空間"上傳至小米手環5作錶盤,則在手機上會甶留存"交錯空間"的本機錶盤資料。


2. 請將手機連接至電腦小米運動APP (4.4.0版) 中小米手環5 "交錯空間"錶盤資料位於電腦目錄

"本機\(手機名稱)\內部儲存空間\Android\data\com.xiaomi.hm.health\files\watch_skin_file\58"

依下載錶盤的多寡,內有一至數個長串英數字的目錄,查看各目錄內的 Png 圖檔可知該目錄是那一個錶盤資料,找到"交錯空間"錶盤圖後,將該目錄下副檔名為 bin 的檔案複製至上面MiBandWFTool_2.1.4解壓縮的目錄內。

3. 將上 bin檔案的檔名更改為易記的英文字 (這裏更改為 Sample.bin,方便後面說明使用)。
4. 將 Sample.bin檔案用滑鼠拖拉至 WatchFace.exe ,則會自動解封錶盤至一個新的Sample目錄。


三、修改錶盤

修改錶盤分為二個方面進行

1. Sample.json:
解封下的目錄有一個副檔名為 json 的版面配置檔案,為文字檔,紀錄各個物件圖檔的座標位置和執行方式,如要修改圖標數值的顯示位置,可修改這裏的文字座標。

2. Png圖檔:
目錄下有很多Png圖檔,錶盤上的每個數字、文字、圖示都是由這些圖檔組成,其經由 json 指定座標顯示,如要將英文的星期改為中文的話,可修改星期Png圖檔內容,共有七個Png檔需修改。

json內的格式,依其英文說明大致都可理解各個物件是做什麼的,對照下方Png縮圖簡單介紹如下:

{
  "Background": {		//背景圖 (尺寸 126X294px 96dpi)
    "Image": {
      "X": 0,			//背景圖 左上坐標 X,Y 為 0,0 位置
      "Y": 0,
      "ImageIndex": 0	//使用 0000.Png 圖檔
    },
    "Preview1": {		//預覽圖 3 張
      "X": 18,
      "Y": 17,
      "ImageIndex": 2
    },
    "Preview2": {
      "X": 18,
      "Y": 17,
      "ImageIndex": 3
    },
    "Preview3": {
      "X": 18,
      "Y": 17,
      "ImageIndex": 1
    }
  },
  "Time": {				//時間數字部份
    "Hours": {			//小時
      "Tens": {			//十位數部份
        "X": 22,		//左上坐標 X,Y 位置
        "Y": 54,
        "ImageIndex": 4,	//使用 0004.png 開始的數字圖檔
        "ImagesCount": 10	//共10 張 (可參考下方圖例) 
      },
      "Ones": {				//小時的個位數部分, 格式同上
        "X": 68,
        "Y": 54,
        "ImageIndex": 4,
        "ImagesCount": 10
      }
    },
    "Minutes": {		//分鐘部份, 格式同小時
      "Tens": {
        "X": 22,
        "Y": 119,
        "ImageIndex": 4,
        "ImagesCount": 10
      },
      "Ones": {
        "X": 68,
        "Y": 119,
        "ImageIndex": 4,
        "ImagesCount": 10
      }
    },
    "TimeDelimiterImage": {		//小時和分鐘間的符號
      "X": 82,
      "Y": 154,
      "ImageIndex": 14
    },
    "UnknownV11": 0				//不明, 都有加上這行
  },
  "Activity": {					//活動圖標
    "Steps": {					//步數
      "Number": {                //用數字表示
        "TopLeftX": 0,			//顯示區塊的左上坐標
        "TopLeftY": 244,
        "BottomRightX": 126,	//顯示區塊的右下坐標
        "BottomRightY": 258,
        "Alignment": "TopCenter",	//步數數字在區塊中對齊的方式
        "SpacingX": 0,				//數字的間距
        "SpacingY": 0,				//數字的行距
        "ImageIndex": 15,			//數字使用 0015.png 開始的圖檔
        "ImagesCount": 10			//共10張
      }
    },
    "UnknownV7": 0
  },
  "Date": {								//日期
    "MonthAndDayAndYear": {
      "OneLine": {				//一行模式, 另有日、月分開的模式 Separate 
        "Number": {
          "TopLeftX": 22,			//顯示區塊語法同上
          "TopLeftY": 184,
          "BottomRightX": 68,
          "BottomRightY": 198,
          "Alignment": "Center",
          "SpacingX": 0,
          "SpacingY": 0,
          "ImageIndex": 15,
          "ImagesCount": 10
        },
        "DelimiterImageIndex": 25
      },
      "TwoDigitsMonth": true,                //是否要雙位數顯示月份
      "TwoDigitsDay": true                    //是否要雙位數顯示日期
}, "DayAmPm": {            //顯示上午AM、下午PM "X": 20, "Y": 35, "ImageIndexAMCN": 47, "ImageIndexPMCN": 48, "ImageIndexAMEN": 49, "ImageIndexPMEN": 50 }, "ENWeekDays": {        //星期 英文語系 "X": 74, "Y": 183, "ImageIndex": 26, "ImagesCount": 7 }, "CNWeekDays": {        //星期 繁體中文語系 "X": 74, "Y": 183, "ImageIndex": 40, "ImagesCount": 7 }, "CN2WeekDays": {        //星期 簡體中文語系 "X": 74, "Y": 183, "ImageIndex": 33, "ImagesCount": 7 } }, "StepsProgress": { //步數圓形圖標的設定 "CircleScale": { "CenterX": 63,            //中心點坐標 "CenterY": 245,             "RadiusX": 36,            //半徑 "RadiusY": 36, "StartAngle": 0,        //起始角度 "EndAngle": 360,        //結束角度 "Width": 4,            //線的寬度 "Color": "0x1EDF9B"    //線的顏色 } }, "Status": { "DoNotDisturb": { //勿擾模式 "Coordinates": { "X": 10, "Y": 10 }, "ImageIndexOn": 52 }, "Lock": { //鎖定模弍 "Coordinates": { "X": 42, "Y": 10 }, "ImageIndexOff": 53 }, "Bluetooth": { //藍芽狀態 "Coordinates": { "X": 26, "Y": 10 }, "ImageIndexOff": 51 } }, "Battery": { //電池顯示 "BatteryText": { "Coordinates": { "TopLeftX": 60, "TopLeftY": 10, "BottomRightX": 96, "BottomRightY": 22, "Alignment": "TopRight", "SpacingX": 0, "SpacingY": 0, "ImageIndex": 54, "ImagesCount": 10 }, "SuffixImageIndex": 64 //附加在電量後的 % 符號 }, "BatteryIcon": { //電池圖標 "X": 96, "Y": 10, "ImageIndex": 65, "ImagesCount": 10        //10張,10段 } } }

舉例如要修改
A. 底圖:更換 0000.png 即可,如將其改為藍色 (PS: Png 圖檔必須設為 96dpi 才能使用)

B. 預覽圖 (手環更換錶盤時用來選擇的圖示):更換 0001.png ~0003.png 即可

C. 日期中日、月的順序:
若為 OneLine 語法 需改為 Separate 語法,如原錶盤即為Separate 語法,更換日、月的坐標即可

D. 一週文字語系更改:
一般內建有多國語系,若沒有的話,如上 0026.png~0032.png 的圖,需用繪圖軟體重新用中文建立


四、封裝錶盤

修改完後,將 Sample.json 檔用滑鼠拖拉至 WatchFace.exe ,則會自動封裝錶盤為 Sample_packed.bin 檔,及幾個錶盤預覽圖片。



五、上傳錶盤(手機請先連接電腦)

1. 手機請先關閉小米運動APP,電腦檔案總管移至手機"交錯空間"的目錄位置
"本機\(手機名稱)\內部儲存空間\Android\data\com.xiaomi.hm.health\files\watch_skin_file\58\b7c4c3e35ca8b10b1ca8e835653580c2"

2. 將上四中封裝好的 Sample_packed.bin檔,檔名改為上 1 內 bin的檔名 b7c4c3e35ca8b10b1ca8e835653580c2.bin

3. 將上四中 Sample_packed_static.Png檔,檔名改為 1內 Png的檔名 fae484120a0f79f234837d1b793af0361585454996816.png

4. 將2、3二個檔案複製回 1的目錄中,覆蓋手機"交錯空間"原始錶盤檔案。

5. 手機打開小米運動APP,至錶盤管理的 本機錶盤 就會看到修改好後的錶盤,就可上傳至小米手環5作錶盤使用。 


從網站 錶盤市場AmazFaces下載的錶盤,一樣是同這個步驟更改檔名,再用電腦複製至手機內覆蓋舊錶盤作更換。

AmazFaces也有提供手機APP:https://amazfitwatchfaces.com/awapp
不過現階段好像還不能直接在Android APP內直接更換錶盤,透過上述電腦連線更換是比較方便操作的...


找到喜歡的錶盤,自己動手換上 朋友、小孩、寵物、動漫...等照片,看時間感覺也會不同喔,祝大家身體健康!


02 小米手環5 錶盤製作


2019年8月12日 星期一

生活小物-藥錠取出器 & 切藥器



< 本文請勿轉載,謝謝 ! >

隨著年齡的增長,父母親也漸漸高齡,身體難免出現一些慢性病需長期服藥控制,大醫院給付的藥錠都是沒有分裝的,看到父母親小心翼翼的從片片的包裝中擠壓取藥,偶爾還是會不小心掉落桌上或地上,而醫院給的藥量都是剛好的,掉到地上的話當月就少一天的藥量,心想難道沒有較好的方法嗎?

上網看了國內不少的取藥器,覺得都不是很好用(純粹個人觀感),想想日本也是一個高齡化的社會,可能會有什麼比較好的發明吧,看了看選擇的樣式還是不多,但是有一款,看到的瞬間我就很想買來用用看...


TOP-001 お薬取出器 トリダス (定價約日幣 2000円,樂天有賣到七折左右)

這個取藥器我已經用了一年多了,真是方便又耐用,除了有時候藥物較小的話會卡一點(晃一晃就會掉下來了),真是好物,藥錠壓出較多時可搭配小鑷子取出。



Fullicon 護立康 集屑切藥器

另外有時候藥劑量是半錠,就需要使用切藥器,剛開始買的是不透明的切藥器,有時藥錠放好切下去時不小心走位,導致切出大小邊,前後也買了好幾個不同的試用,而下面這個雖然定價較其它切藥器貴了一些,但物有所值,推薦使用,我是去其經銷藥局買的(非廣告喔)。


買了 藥錠取出器 和 切藥器 後,也順便買了 一週七日份藥盒,直接幫父母親將一週會用的藥物分裝好,這樣用藥部份就較沒問題了。一週藥盒我是買28格便宜的那種,直觀好拿取,但這種盒蓋接合處久了後(半年左右)會變軟或斷裂,算是消耗品,也沒什麼較好可推薦的,多買一個備用就好。


PS:藥錠取出器有出一個 Pro 專業版,有需要的可參考看看

2019年8月11日 星期日

積木式免焊接 記憶型空調紅外線轉譯器 - 04 紀錄 & 修正篇

< 本文請勿轉載,謝謝 ! >

本文紀錄網友回報的適用冷氣空調機型,另程式碼貼於下方,有問題可方便討論除錯修正。

適用機型:

1. 三菱電機:機型 GE25NA / 遙控器 KM10D


記憶型空調紅外線轉譯器 程式碼

24行:移除雙斜線,會進入除錯模式,可在Arduino IDE 監控視窗查看轉譯器訊號接收、發射狀況
25行:移除雙斜線,紅外線發射時主板會有閃燈及嗶聲,初期可監聽看是否穩定執行
26行:移除雙斜線,焊接的擴展板S1按鍵可重發行目前的紅外線信號,方便測試用(電子積木版勿開啟,會造成連續誤觸發)

程式下載 (密碼藍字 mygraphpaper.com https://www.mygraphpaper.com/download/MPG_TR_Translator.rar
/*
   IRremote
   An IR LED must be connected to Arduino PWM pin 3.
   Copyright Ken Shirriff
   http://arcfn.com

   MPG_IR_Translator Ver.1.0  2019.8.11 By MGP. 
   Only for Air conditioner.  https://www.mygraphpaper.com blog.  
   
   Do not reprint,please.
   This site is only for personal use, pay attention to safety when using, please.
   Disclaimer : This site will not bear any liability for any errors or omissions.
                If you agree, use the contents of this site.
   請勿轉載.
   本網站內容及程式僅提供個人非營利使用, 使用時請注意安全.
   免責聲明 : 本網站不會對任何錯誤或遺漏承擔任何賠償責任, 若您同意再使用本網站、程式內容。

   請注意執行本程式前, 請使用同壓縮檔內檔案覆蓋 IRremoteInt.h , 或手動修改參數值如下:
   Line 43 : #define RAWBUF  624    // 預設儲存信號長度為 101 個 , 但冷氣機編碼超過, 需加大
   Line 52 : int rawlen;            // 冷氣機編碼的大小超過 Byte 255計數, 改為 int 
   Line 98 : #define _GAP    30000  // 預設信號結束的空白時間 5000, 但太小導致取樣不到 RPT_SPACE 的間隔參數, 需加大
*/

//#define MPG_DEBUG       //Arduino監控視窗除錯用, 若您的紅外線信號記錄不到可打開監看取樣值.
//#define MPG_SendHit     //紅外線傳送時會發出提示音及閃燈, 但會延遲發射時間
//#define MPG_SOLDER      //焊接版本可打開以使用擴展板S1測試按鍵, 電子積木版勿開會造成誤觸發.

//各家紅外線編碼不同, 靈敏度調整用, 非必要請勿修改
#define Tolerance_scope       4       //MARK, SPACE 取樣誤差判斷容忍範圍, 4X50us=200us
#define MARK_EXCESS_Factor    2       //MARK, SPACE MARK_EXCESS 參數取樣誤差修正 100us/2=50us

//--------------------------------------------
#include <IRremote.h>
#include <EEPROM.h>

#define recvPin              2        //紅外線接收在 Pin2; 發射內定在 Pin3
#define Pin9_SW              9        //Pin9  擴展板S1按鍵 ,發射當前紅外線信號測試用
#define Recv_Enable_LED_Pin  10       //Pin10 擴展板啟動指示燈
#define LED_Buzzer_Pin       11       //Pin11 動作指示燈及聲音, Pin13同動作
#define khz                  38       //使用 38kHz 頻率發射 RAW
#define SMax                 10       //MARK, SPACE 取樣排序陣列大小

//米家APP內創維機頂盒 遙控器數字 "0~9 確定" 的NEC編碼
#define IR_NUM_SIZE  11           //使用 "0~9 確定" ,共11個按鍵 (注意 變更這個值會初始化歸零 EEPROM, 清除已記錄儲存的紅外線按鍵信號)
#define N00     0x80BFE11E        //數字盤 0
#define N01     0x80BF49B6
#define N02     0x80BFC936
#define N03     0x80BF33CC
#define N04     0x80BF718E
#define N05     0x80BFF10E
#define N06     0x80BF13EC
#define N07     0x80BF51AE
#define N08     0x80BFD12E
#define N09     0x80BF23DC
#define N10     0x80BF738C        //數字盤 確定

struct NUM_MAP {                  //將 "0~9 確定" 的紅外線編碼對應成數字 0-10 ,方便比對搜尋
  unsigned long IR_CODE;
  byte INDEX_NUM;
};
struct NUM_MAP IR_MAP[IR_NUM_SIZE] = {{N00, 0},  {N01, 1},  {N02, 2},  {N03, 3},  {N04, 4},  {N05, 5},  {N06, 6},  {N07, 7},  {N08, 8},  {N09, 9},  {N10, 10}};
      
struct IR_RAW_type {              //原生信號參數結構 (注意 變更這個結構大小會初始化歸零 EEPROM, 清除已記錄儲存的紅外線按鍵信號)
  char RAW_TYPE;                  //紅外線編碼類別 UNKNOWN, NEC, SONY ...
  unsigned int RAW_HDR_MARK ;     //初始信號 RAW_HDR_MARK + RAW_HDR_SPACE
  unsigned int RAW_HDR_SPACE ;
  unsigned int RAW_BIT_MARK ;     //0 與 1 的信號組合
  unsigned int RAW_ONE_SPACE ;    //1 = RAW_BIT_MARK + RAW_ONE_SPACE
  unsigned int RAW_ZERO_SPACE ;   //0 = RAW_BIT_MARK + RAW_ZERO_SPACE
  unsigned int rawData_Bits ;     //傳送位元數, 可能會過 255 (624/2=312)
  unsigned int rawHole[8][3] ;    //特殊長度的 位置,數值(MARK+SPACE), 如 PRT_MARK+RPT_SPACE
  byte rawHole_Count ;            //rawHole陣列大小
  byte rawData_Count ;            //rawData陣列大小
  byte rawData[39] ;              //可記錄至 39X16 = 624bit, 要記錄10組信號, EEPROM只有1024byte, 總結構大小不能超過 1024/10 = 102byte
};
struct IR_RAW_type IR_RAW;

bool EnterSetup = false;            //是否進入設定模式
byte SetupIndex = 0;                //目前的設定數字
byte IR_MAP_Index;                  //儲存找到對應的 0-9

IRrecv irrecv(recvPin);             //設定接收用 Pin
IRsend irsend;                      //發射用
decode_results results;             //儲存接收結果用


//以下為程式碼--------------------------------------------------------------------------------------------------------------------------


//控制 動作指示燈及聲音 Pin11、13
void ST_LED_Buzzer (int Send_Status) {
  while ( Send_Status > 0 ) {
    if (Send_Status >= 5) {
      digitalWrite(LED_Buzzer_Pin, HIGH);
      digitalWrite(13, HIGH);
      delay(400);
      digitalWrite(LED_Buzzer_Pin, LOW);
      digitalWrite(13, LOW);
      delay(400);
      Send_Status = Send_Status - 5;
    } else {
      digitalWrite(LED_Buzzer_Pin, HIGH);
      digitalWrite(13, HIGH);
      delay(150);
      digitalWrite(LED_Buzzer_Pin, LOW);
      digitalWrite(13, LOW);
      delay(150);
      Send_Status = Send_Status - 1;
    }
  }
}


//發送SONY紅外線編碼陣列 (SONY編碼較特殊, 變動MARK)
void SONYIRSend(byte SendRAWData[], unsigned int SendRAWData_Bits) {
  unsigned long SONYIRCode;
  SONYIRCode = ( (((unsigned long)SendRAWData[0]) << 24) | (((unsigned long)SendRAWData[1]) << 16) | (((unsigned long)SendRAWData[2]) << 8) | (((unsigned long)SendRAWData[3])) );
  irsend.sendSony(SONYIRCode , SendRAWData_Bits);
}

//發送紅外線編碼陣列, 位元由左至右發送, 資料共計 rawData_Bits 位元
//void RAWIRSend(發送陣列資料, 陣列大小, 發射位元數, 發射頻率)
void RAWIRSend(byte SendRAWData[], byte SendRAWData_Len, unsigned int SendRAWData_Bits, byte hz) {
  unsigned int rawData_Bits_Count =0;
  byte Mask = 1; 
  byte rawHole_ind = 0;
    
  irsend.enableIROut(hz);  // khz
  irsend.space(max(IR_RAW.RAW_HDR_MARK,IR_RAW.RAW_HDR_SPACE));  //Initial signal

  irsend.mark(IR_RAW.RAW_HDR_MARK);
  irsend.space(IR_RAW.RAW_HDR_SPACE);
  for (unsigned int i = 0; i < SendRAWData_Len; i++) {
    for (Mask = B10000000; Mask > 0; Mask >>= 1) {
      if (rawData_Bits_Count < SendRAWData_Bits) {
        if ((rawHole_ind < IR_RAW.rawHole_Count) && (rawData_Bits_Count == IR_RAW.rawHole[rawHole_ind][0])){
            //Special length
            irsend.mark(IR_RAW.rawHole[rawHole_ind][1]);
            irsend.space(IR_RAW.rawHole[rawHole_ind][2]);
            rawHole_ind ++ ;
        } else {
            if (SendRAWData[i] & Mask) {
            //Bit 1
            irsend.mark(IR_RAW.RAW_BIT_MARK);
            irsend.space(IR_RAW.RAW_ONE_SPACE);
            }
            else {
              //Bit 0
              irsend.mark(IR_RAW.RAW_BIT_MARK);
              irsend.space(IR_RAW.RAW_ZERO_SPACE);
            }
        }
        rawData_Bits_Count++;
      }
    }
  }
  irsend.space(max(IR_RAW.RAW_HDR_MARK,IR_RAW.RAW_HDR_SPACE));  //Stop signal
}


//取樣陣列右移排大小
unsigned int Probability[SMax], Temporary[SMax];  //取樣陣列 數值, 次數
void Shift_Val(unsigned int Val){
  byte i,j;
  
  for(i = 0; i < SMax; i++) {    //將較小數值向後移
    if ((i>0) && (Val == Probability[i-1])) break;
    if (Val > Probability[i]) {
      for(j = SMax-1; j >= i+1; j--) {
        Probability[j] = Probability[j-1];
        Temporary[j] = Temporary[j-1];
      }
      Probability[i] = Val;
      Temporary[j] = 0;
      break;
    } 
  }

  for(i = 0; i<SMax; i++) {    //記錄出現次數
    if (Val == Probability[i]) {
      Temporary[i]++;
      break;
    }
  }
}


//統計原生訊號出現次數
//FindMaxCount(原始編碼訊號, 處理編碼訊號陣列起始位置, 是否需比對 MARK 值, 判斷是ONE_SPACE的判斷值)
unsigned int FindMaxCount(decode_results *results, int StartPos, unsigned int Check_RAW_BIT_MARK, unsigned int MAX_RAW_ZERO_SPACE) {
  unsigned int Return_MAX_Value = 0;  //回傳值
  
  for (byte i=0 ; i < SMax ; i++) {
    Probability[i]=0; //初始陣列
    Temporary[i]=0;
  }
  
  for (int i = StartPos; i < results->rawlen; i+=2) {   //results->rawlen 為int
    if ((Check_RAW_BIT_MARK == 0) || (results->rawbuf[i-1] == Check_RAW_BIT_MARK)){ //是否需比對 MARK 值
      if ((MAX_RAW_ZERO_SPACE == 0) || (results->rawbuf[i] > MAX_RAW_ZERO_SPACE)){ //判斷 Space 長的判斷值     
        Shift_Val(results->rawbuf[i]);    //取樣陣列排大小
      }
    }
  }

  //從最小值找向連續的最大值, 出現最多次的
  unsigned int Last_Count = 0;
  for (int i = SMax-1; i>=0; i--) {
    if ((Probability[i] >= Tolerance_scope) && (Temporary[i] > 0)) {   //有信號
      if (Return_MAX_Value == 0) {
        Return_MAX_Value = Probability[i];
        Last_Count = Temporary[i];
      } else {
          if ((Probability[i] - Probability[i+1]) <= Tolerance_scope) { //連續誤差訊號
            if (Temporary[i] >= Last_Count) {
              Return_MAX_Value = Probability[i];
              Last_Count = Temporary[i];
            }
          } else {
            break;    //信號不連續     
          }
      }
    }
  }    
 
  return Return_MAX_Value;
}


//取得原生訊號參數及重新編碼
void EncodeRAWCode (decode_results *results) {
  IR_RAW.rawData_Count = 0;
  IR_RAW.rawHole_Count = 0 ;  
  IR_RAW.RAW_TYPE = results->decode_type;
  
#ifdef MPG_DEBUG  
  Serial.println(F(""));
  Serial.print(F("IR Struct Size (byte) : "));  
  Serial.println(sizeof(IR_RAW),DEC);
  Serial.print(F("IR TYPE : "));  
  Serial.println(IR_RAW.RAW_TYPE,DEC);   //4 SONY 
#endif
 
  //SONY使用40Khz, 且是改變 MARK的長度, SPACE不變, 直接使用內建函數解碼, 用38K可以接收但失敗率較大
  //SONY發送為 unsigned long 4Byte, 拆解入 IR_RAW.rawData 存放
  if (IR_RAW.RAW_TYPE == SONY) {
    IR_RAW.RAW_HDR_MARK = 2400;
    IR_RAW.RAW_HDR_SPACE = 600;
    IR_RAW.RAW_BIT_MARK = 600;
    IR_RAW.RAW_ZERO_SPACE = 600;
    IR_RAW.RAW_ONE_SPACE = 1200;    
    IR_RAW.rawData_Bits = results->bits;
    IR_RAW.rawData_Count = 4;
    IR_RAW.rawData[0] = (byte)((results->value & 0xff000000) >> 24);
    IR_RAW.rawData[1] = (byte)((results->value & 0x00ff0000) >> 16);
    IR_RAW.rawData[2] = (byte)((results->value & 0x0000ff00) >> 8);   
    IR_RAW.rawData[3] = (byte)((results->value & 0x000000ff));
  } else {
      IR_RAW.rawData_Bits = (results->rawlen-3)/2;  //扣除頭2尾1, 2個信號為 1bit
        
      IR_RAW.RAW_HDR_MARK = results->rawbuf[1];  //第一個訊號[0]無效,[1]為RAW_HDR_MARK
      IR_RAW.RAW_HDR_SPACE = results->rawbuf[2]; //[2]RAW_HDR_SPACE
      
    #ifdef MPG_DEBUG  
      Serial.println(F(""));
      Serial.println(F("// My Data Start , Mark & Ctrl-C Copy ---------------")); 
      Serial.println(F(""));  
      Serial.print(F("#define RAW_HDR_MARK    "));
      Serial.print(IR_RAW.RAW_HDR_MARK * USECPERTICK - MARK_EXCESS/MARK_EXCESS_Factor, DEC);  
      Serial.println(F(""));  
      Serial.print(F("#define RAW_HDR_SPACE   "));
      Serial.print(IR_RAW.RAW_HDR_SPACE * USECPERTICK + MARK_EXCESS/MARK_EXCESS_Factor, DEC); 
    #endif   
    
      //取得 BIT_MARK , 3開始的奇數位置為 RAW_BIT_MARK 值
      IR_RAW.RAW_BIT_MARK = FindMaxCount(results , 3, 0, 0);
      
    #ifdef MPG_DEBUG    
      Serial.println(F(""));   
      Serial.print(F("#define RAW_BIT_MARK    "));
      Serial.print(IR_RAW.RAW_BIT_MARK * USECPERTICK - MARK_EXCESS/MARK_EXCESS_Factor, DEC);
      Serial.print(F("     // "));
      for(byte i = 0; i < SMax; i++) {
        if (Probability[i] > 0 ) {
          Serial.print(F("( "));
          Serial.print(Probability[i] * USECPERTICK, DEC);
          Serial.print(F(" , "));
          Serial.print(Temporary[i] , DEC);    
          Serial.print(F(" ), "));     
        }
      }
      Serial.print(F(" times."));  
    #endif
    
      //取得 ZERO_SPACE , 最小值範圍中最多出現次數, 4開始的偶數位置為 RAW_ZERO_SPACE 值
      IR_RAW.RAW_ZERO_SPACE = FindMaxCount(results , 4, IR_RAW.RAW_BIT_MARK, 0);  
    
    #ifdef MPG_DEBUG  
      Serial.println(F("")); 
      Serial.print(F("#define RAW_ZERO_SPACE  "));
      Serial.print(IR_RAW.RAW_ZERO_SPACE * USECPERTICK + MARK_EXCESS/MARK_EXCESS_Factor, DEC);
      Serial.print(F("     // "));
      for(byte i = 0; i < SMax; i++) {
        if (Probability[i] > 0 ) {
          Serial.print(F("( "));
          Serial.print(Probability[i] * USECPERTICK, DEC);
          Serial.print(F(" , "));
          Serial.print(Temporary[i] , DEC);    
          Serial.print(F(" ), "));     
        }
      }
      Serial.print(F(" times. (in RAW_BIT_MARK)")); 
    #endif
    
      //取得 ONE_SPACE , 最小值範圍中最多出現次數, 4開始的偶數位置為 RAW_ONE_SPACE 值
      //ONE_SPACE 的下限值, 若仍太小可漸加大Tolerance_scope測試
      unsigned int MAX_RAW_ZERO_SPACE = IR_RAW.RAW_ZERO_SPACE + Tolerance_scope;
      IR_RAW.RAW_ONE_SPACE = FindMaxCount(results , 4, IR_RAW.RAW_BIT_MARK, MAX_RAW_ZERO_SPACE); 
    
    #ifdef MPG_DEBUG  
      Serial.println(F("")); 
      Serial.print(F("#define RAW_ONE_SPACE   "));
      Serial.print(IR_RAW.RAW_ONE_SPACE * USECPERTICK + MARK_EXCESS/MARK_EXCESS_Factor, DEC);
      Serial.print(F("     // "));
      for(byte i = 0; i < SMax; i++) {
        if (Probability[i] > 0 ) {
          Serial.print(F("( "));
          Serial.print(Probability[i] * USECPERTICK, DEC);
          Serial.print(F(" , "));
          Serial.print(Temporary[i] , DEC);    
          Serial.print(F(" ), "));     
        }
      }
      Serial.print(F(" times. (in RAW_BIT_MARK)"));  
    #endif  
      
      //將 Mark+Space 的 0 1 編碼為 Byte 型態存放
      byte rawTemp = 0; //暫存值
      byte Bits_Count = 0;  //目前 Bit 計數
      MAX_RAW_ZERO_SPACE = (IR_RAW.RAW_ONE_SPACE + IR_RAW.RAW_ZERO_SPACE) / 2;  //取中間值作 0 1 判斷
      for (int i = 4; i < results->rawlen; i+=2) {
        Bits_Count++;
        rawTemp = rawTemp << 1;
        if (results->rawbuf[i] > MAX_RAW_ZERO_SPACE) rawTemp = rawTemp | 1;
        if (Bits_Count==8) {    //完成 1 Byte 存放入rawData
          IR_RAW.rawData[IR_RAW.rawData_Count] = rawTemp;
          Bits_Count = 0;
          IR_RAW.rawData_Count ++;
          rawTemp = 0;
        }
      }
    
      if (Bits_Count != 0) {    //不足 1Byte 的補完
          for (byte i = (Bits_Count+1); i <= 8; i++) {
            rawTemp = rawTemp << 1;
          }
          IR_RAW.rawData[IR_RAW.rawData_Count] = rawTemp;
          IR_RAW.rawData_Count ++;
      }
    
      //掃描特殊長度
      byte Max_rawHole = sizeof(IR_RAW.rawHole)/sizeof(unsigned int)/3;    
      for (int i = 3; i < results->rawlen-1; i++) {
        if(i%2==1){   //MARK
              if (!((results->rawbuf[i]>=IR_RAW.RAW_BIT_MARK-Tolerance_scope) && (results->rawbuf[i]<=IR_RAW.RAW_BIT_MARK+Tolerance_scope))) {
                if (IR_RAW.rawHole_Count >= Max_rawHole) {
                  results->overflow = true;   //特殊編碼太多, 無空間可記錄, 報溢位錯誤
                  break;
                }
                IR_RAW.rawHole[IR_RAW.rawHole_Count][0] = (i-3)/2;  //0起算
                IR_RAW.rawHole[IR_RAW.rawHole_Count][1] = results->rawbuf[i] * USECPERTICK;
                IR_RAW.rawHole[IR_RAW.rawHole_Count][2] = results->rawbuf[i+1] * USECPERTICK;
                IR_RAW.rawHole_Count++;
                i++;
              }
        } else {    //SPACE
              if (results->rawbuf[i]>(IR_RAW.RAW_ONE_SPACE + Tolerance_scope)) {
                if (IR_RAW.rawHole_Count >= Max_rawHole) {
                  results->overflow = true;   //特殊編碼太多, 無空間可記錄, 報溢位錯誤
                  break;
                }
                IR_RAW.rawHole[IR_RAW.rawHole_Count][0] = (i-4)/2;   //0起算      
                IR_RAW.rawHole[IR_RAW.rawHole_Count][1] = results->rawbuf[i-1] * USECPERTICK;
                IR_RAW.rawHole[IR_RAW.rawHole_Count][2] = results->rawbuf[i] * USECPERTICK;
                IR_RAW.rawHole_Count++;
              }      
        }
      }

      //做硬體取樣延遲誤差修正
      IR_RAW.RAW_HDR_MARK    = IR_RAW.RAW_HDR_MARK * USECPERTICK - MARK_EXCESS/MARK_EXCESS_Factor;   //次數 X 間隔(50us) - 誤差
      IR_RAW.RAW_HDR_SPACE   = IR_RAW.RAW_HDR_SPACE * USECPERTICK + MARK_EXCESS/MARK_EXCESS_Factor;
      IR_RAW.RAW_BIT_MARK    = IR_RAW.RAW_BIT_MARK * USECPERTICK - MARK_EXCESS/MARK_EXCESS_Factor;
      IR_RAW.RAW_ONE_SPACE   = IR_RAW.RAW_ONE_SPACE * USECPERTICK + MARK_EXCESS/MARK_EXCESS_Factor; 
      IR_RAW.RAW_ZERO_SPACE  = IR_RAW.RAW_ZERO_SPACE * USECPERTICK + MARK_EXCESS/MARK_EXCESS_Factor;  
  }
#ifdef MPG_DEBUG  
  Serial.println(F(""));
  Serial.println(F(""));  
  Serial.print(F("int rawData_Bits = ")); 
  Serial.print(IR_RAW.rawData_Bits, DEC); 
  Serial.print(F(" ;"));   
  Serial.println(F(""));   
  Serial.print(F("byte rawData["));
  Serial.print(IR_RAW.rawData_Count, DEC);
  Serial.print(F("] = { "));

  for (byte i = 0;  i < IR_RAW.rawData_Count;  i++) {
    Serial.print(F("0x"));
    Serial.print(IR_RAW.rawData[i], HEX);
    if ( i < IR_RAW.rawData_Count-1 ) Serial.print(F(", "));
  }
  Serial.println(F(" };"));

  Serial.print(F("unsigned int rawHole["));
  Serial.print(IR_RAW.rawHole_Count, DEC);
  Serial.print(F("][3] = { "));

  for (byte i = 0;  i < IR_RAW.rawHole_Count;  i++) {
    Serial.print(F("{ "));
    for (byte j = 0;  j < 3;  j++) { 
      Serial.print(IR_RAW.rawHole[i][j], DEC); 
      if ( j < 2 ) Serial.print(F(", ")); 
    }
    Serial.print(F(" } "));
    if ( i < IR_RAW.rawHole_Count-1 ) Serial.print(F(", "));
  }
  Serial.println(F("};"));
  Serial.println(F(""));
    
  Serial.println(F("// My Data End --------------- By MGP."));  
  Serial.println(F(""));  
#endif
}


//初始設定
void setup() {
  Serial.begin(9600);                     //與電腦序列埠通訊連接用
  irrecv.enableIRIn();                    //充許接收紅外線信號

  SetupIndex = 0;  
  IR_RAW.rawData_Count = 0; 
  if ((EEPROM.read(1022) != IR_NUM_SIZE) || (EEPROM.read(1023) != sizeof(IR_RAW))) {   //第一次使用, 初始化歸零 EEPROM
    Serial.println(F("Wait for initial EEPROM ..."));     
    for(int i =0; i<1022; i++) EEPROM.write(i,0);
    EEPROM.write(1022,IR_NUM_SIZE);         //標記 EEPROM 結構個數
    EEPROM.write(1023,sizeof(IR_RAW));      //標記 EEPROM 結構大小      
  }

  pinMode(Pin9_SW, INPUT);                //初始 擴展板S1按鍵 腳位
  pinMode(Recv_Enable_LED_Pin, OUTPUT);   //初始 啟動指示燈 腳位
  pinMode(LED_Buzzer_Pin, OUTPUT);        //初始 動作指示燈及聲音 腳位
  pinMode(13, OUTPUT);                    //初始 開發板指示燈 腳位
   
  digitalWrite(Recv_Enable_LED_Pin, HIGH);//打開 啟動擴展板指示燈, 程式作用中, 要關閉請改 LOW
  digitalWrite(LED_Buzzer_Pin, LOW);      //關閉 動作指示燈及聲音
  digitalWrite(13, LOW);                  //關閉 主板指示燈, 相容電子積木版用, 同 LED_Buzzer_Pin

  ST_LED_Buzzer (1);                      //通知已啟動開始接收
  Serial.println(F("MPG IR-Translator is Ready."));   
}


//主程式迴圈
void loop() {
#ifdef MPG_SOLDER    //焊接擴展版 S1按鈕
  if (digitalRead(Pin9_SW) == LOW) {  
    Serial.println(sizeof(IR_RAW));
    delay(100);
    ST_LED_Buzzer (1);
    RAWIRSend(IR_RAW.rawData, IR_RAW.rawData_Count, IR_RAW.rawData_Bits, khz);
    #ifdef MPG_DEBUG
      show_RAW_IR();
    #endif
    irrecv.enableIRIn();
    delay(100);   
  }
#endif

  if (irrecv.decode(&results)) {         //是否偵測到紅外線信號
    IR_MAP_Index = IR_NUM_SIZE;          //預設IR_MAP_Index
    if (results.decode_type == NEC) {    //判斷為哪個數字鍵, 只偵測 NEC 編碼
#ifdef MPG_DEBUG
      Serial.print(F("NEC Code : "));       //提供序列埠監看
      Serial.print(results.value, HEX);
#endif      
      for (byte i = 0; i < IR_NUM_SIZE; i++) {
        if (results.value == IR_MAP[i].IR_CODE) {
          IR_MAP_Index = IR_MAP[i].INDEX_NUM;
          break;
        }
      }
 #ifdef MPG_DEBUG     
      Serial.print(F("  Map Index : "));
      Serial.println(IR_MAP_Index, DEC);
#endif     
      if (IR_MAP_Index < IR_NUM_SIZE) { //是否有找到對應的數字鍵
        delay(100);                     //收發間隔,穩定用
        switch (IR_MAP_Index) {         //不合併,保留每個數字對應的程式操作空間
          case 0:
            if(EnterSetup) {
              ST_LED_Buzzer (10);
              SetupIndex = IR_MAP_Index;
            } else {          
              #ifdef MPG_SendHit    
                ST_LED_Buzzer (1);     //蜂鳴器先叫, 以免和冷氣混在一起
              #endif
              EEPROM_readAnything(IR_MAP_Index*sizeof(IR_RAW), IR_RAW);
              if (IR_RAW.RAW_TYPE == SONY) SONYIRSend(IR_RAW.rawData, IR_RAW.rawData_Bits);
              else RAWIRSend(IR_RAW.rawData, IR_RAW.rawData_Count, IR_RAW.rawData_Bits, khz); 
              #ifdef MPG_DEBUG
                show_RAW_IR();
              #endif
            }
            break;
          case 1:
            if(EnterSetup) {
              ST_LED_Buzzer (IR_MAP_Index);
              SetupIndex = IR_MAP_Index;
            } else {            
              #ifdef MPG_SendHit    
                ST_LED_Buzzer (1);     //蜂鳴器先叫, 以免和冷氣混在一起
              #endif
              EEPROM_readAnything(IR_MAP_Index*sizeof(IR_RAW), IR_RAW); 
              if (IR_RAW.RAW_TYPE == SONY) SONYIRSend(IR_RAW.rawData, IR_RAW.rawData_Bits);
              else RAWIRSend(IR_RAW.rawData, IR_RAW.rawData_Count, IR_RAW.rawData_Bits, khz);              
              #ifdef MPG_DEBUG
                show_RAW_IR();
              #endif
            }
            break;
          case 2:
            if(EnterSetup) {
              ST_LED_Buzzer (IR_MAP_Index);
              SetupIndex = IR_MAP_Index;
            } else {            
              #ifdef MPG_SendHit    
                ST_LED_Buzzer (1);     //蜂鳴器先叫, 以免和冷氣混在一起
              #endif
              EEPROM_readAnything(IR_MAP_Index*sizeof(IR_RAW), IR_RAW);              
              if (IR_RAW.RAW_TYPE == SONY) SONYIRSend(IR_RAW.rawData, IR_RAW.rawData_Bits);
              else RAWIRSend(IR_RAW.rawData, IR_RAW.rawData_Count, IR_RAW.rawData_Bits, khz);              
              #ifdef MPG_DEBUG
                show_RAW_IR();
              #endif
            }
            break;
          case 3: 
            if(EnterSetup) {
              ST_LED_Buzzer (IR_MAP_Index);
              SetupIndex = IR_MAP_Index;
            } else {        
              #ifdef MPG_SendHit    
                ST_LED_Buzzer (1);     //蜂鳴器先叫, 以免和冷氣混在一起
              #endif
              EEPROM_readAnything(IR_MAP_Index*sizeof(IR_RAW), IR_RAW);              
              if (IR_RAW.RAW_TYPE == SONY) SONYIRSend(IR_RAW.rawData, IR_RAW.rawData_Bits);
              else RAWIRSend(IR_RAW.rawData, IR_RAW.rawData_Count, IR_RAW.rawData_Bits, khz);             
              #ifdef MPG_DEBUG
                show_RAW_IR();
              #endif
            }
            break;
          case 4:
            if(EnterSetup) {
              ST_LED_Buzzer (IR_MAP_Index);
              SetupIndex = IR_MAP_Index;

            } else {            
              #ifdef MPG_SendHit    
                ST_LED_Buzzer (1);     //蜂鳴器先叫, 以免和冷氣混在一起
              #endif
              EEPROM_readAnything(IR_MAP_Index*sizeof(IR_RAW), IR_RAW);              
              if (IR_RAW.RAW_TYPE == SONY) SONYIRSend(IR_RAW.rawData, IR_RAW.rawData_Bits);
              else RAWIRSend(IR_RAW.rawData, IR_RAW.rawData_Count, IR_RAW.rawData_Bits, khz);               
              #ifdef MPG_DEBUG
                show_RAW_IR();
              #endif
            }
            break;
          case 5:
            if(EnterSetup) {
              ST_LED_Buzzer (IR_MAP_Index);
              SetupIndex = IR_MAP_Index;
            } else {            
              #ifdef MPG_SendHit    
                ST_LED_Buzzer (1);     //蜂鳴器先叫, 以免和冷氣混在一起
              #endif
              EEPROM_readAnything(IR_MAP_Index*sizeof(IR_RAW), IR_RAW);              
              if (IR_RAW.RAW_TYPE == SONY) SONYIRSend(IR_RAW.rawData, IR_RAW.rawData_Bits);
              else RAWIRSend(IR_RAW.rawData, IR_RAW.rawData_Count, IR_RAW.rawData_Bits, khz);              
              #ifdef MPG_DEBUG
                show_RAW_IR();
              #endif
            }
            break;
          case 6:
            if(EnterSetup) {
              ST_LED_Buzzer (IR_MAP_Index);
              SetupIndex = IR_MAP_Index;
            } else {            
              #ifdef MPG_SendHit    
                ST_LED_Buzzer (1);     //蜂鳴器先叫, 以免和冷氣混在一起
              #endif
              EEPROM_readAnything(IR_MAP_Index*sizeof(IR_RAW), IR_RAW);              
              if (IR_RAW.RAW_TYPE == SONY) SONYIRSend(IR_RAW.rawData, IR_RAW.rawData_Bits);
              else RAWIRSend(IR_RAW.rawData, IR_RAW.rawData_Count, IR_RAW.rawData_Bits, khz);             
              #ifdef MPG_DEBUG
                show_RAW_IR();
              #endif
            }
            break;
          case 7:                          
            if(EnterSetup) {
              ST_LED_Buzzer (IR_MAP_Index);
              SetupIndex = IR_MAP_Index;
            } else {            
              #ifdef MPG_SendHit    
                ST_LED_Buzzer (1);     //蜂鳴器先叫, 以免和冷氣混在一起
              #endif
              EEPROM_readAnything(IR_MAP_Index*sizeof(IR_RAW), IR_RAW);              
              if (IR_RAW.RAW_TYPE == SONY) SONYIRSend(IR_RAW.rawData, IR_RAW.rawData_Bits);
              else RAWIRSend(IR_RAW.rawData, IR_RAW.rawData_Count, IR_RAW.rawData_Bits, khz);              
              #ifdef MPG_DEBUG
                show_RAW_IR();
              #endif
            }
            break;
          case 8:
            if(EnterSetup) {
              ST_LED_Buzzer (IR_MAP_Index);
              SetupIndex = IR_MAP_Index;
            } else {            
              #ifdef MPG_SendHit    
                ST_LED_Buzzer (1);     //蜂鳴器先叫, 以免和冷氣混在一起
              #endif
              EEPROM_readAnything(IR_MAP_Index*sizeof(IR_RAW), IR_RAW);              
              if (IR_RAW.RAW_TYPE == SONY) SONYIRSend(IR_RAW.rawData, IR_RAW.rawData_Bits);
              else RAWIRSend(IR_RAW.rawData, IR_RAW.rawData_Count, IR_RAW.rawData_Bits, khz);              
              #ifdef MPG_DEBUG
                show_RAW_IR();
              #endif
            }
            break;
          case 9:
            if(EnterSetup) {
              ST_LED_Buzzer (IR_MAP_Index);
              SetupIndex = IR_MAP_Index;
            } else {            
              #ifdef MPG_SendHit    
                ST_LED_Buzzer (1);     //蜂鳴器先叫, 以免和冷氣混在一起
              #endif
              EEPROM_readAnything(IR_MAP_Index*sizeof(IR_RAW), IR_RAW);              
              if (IR_RAW.RAW_TYPE == SONY) SONYIRSend(IR_RAW.rawData, IR_RAW.rawData_Bits);
              else RAWIRSend(IR_RAW.rawData, IR_RAW.rawData_Count, IR_RAW.rawData_Bits, khz);               
              #ifdef MPG_DEBUG
                show_RAW_IR();
              #endif
            }
            break;
          case 10:    //進入設定模式            
            if(EnterSetup) {
              ST_LED_Buzzer (20);
              EnterSetup = false;
            } else {
              ST_LED_Buzzer (15);
              EnterSetup = true;
            }
            break;            
        }
        irrecv.enableIRIn();    //IRRemote 在發射的時候會把接收功能停掉, 重開接收
      }
    } 

    if (IR_MAP_Index == IR_NUM_SIZE) {   //非數字鍵
      if(EnterSetup) {
        if (results.overflow)  {   //位元數太多, 無法記錄    
          #ifdef MPG_DEBUG    
            dumpInfo(&results);
            dumpRaw(&results);         
            show_RAW_IR();
          #endif
        } else {
          #ifdef MPG_DEBUG    
            dumpInfo(&results);
            dumpRaw(&results);
          #endif        
          EncodeRAWCode(&results);
          if ((IR_RAW.RAW_BIT_MARK >0) && (IR_RAW.RAW_ONE_SPACE >0) && (IR_RAW.RAW_ZERO_SPACE >0) && (results.overflow == false)) {
            EEPROM_writeAnything(SetupIndex*sizeof(IR_RAW), IR_RAW);
            ST_LED_Buzzer(2);     //提示已記錄按鍵紅外線值
          }
        }
      }          
    }    
    irrecv.resume();    //重新接收
    delay(100);         //穩定用
  }
}


//EEPROM 函數
template <class T> int EEPROM_writeAnything(int ee, const T& value) {
  const byte* p = (const byte*)(const void*)&value;
  unsigned int i;
  for (i = 0; i < sizeof(value); i++)
    EEPROM.write(ee++, *p++);
  return i;
}

template <class T> int EEPROM_readAnything(int ee, T& value) {
  byte* p = (byte*)(void*)&value;
  unsigned int i;
  for (i = 0; i < sizeof(value); i++)
    *p++ = EEPROM.read(ee++);
  return i;
}

//------- 以下為除錯用函數
//顯示目前RAW結構參數
void show_RAW_IR(){
  Serial.println(F(""));
  Serial.println(F("// My Data Start , Mark & Ctrl-C Copy ---------------")); 
  Serial.println(F("")); 
  Serial.print(F("#define RAW_HDR_MARK    "));
  Serial.print(IR_RAW.RAW_HDR_MARK, DEC);  
  Serial.println(F(""));  
  Serial.print(F("#define RAW_HDR_SPACE   "));
  Serial.print(IR_RAW.RAW_HDR_SPACE, DEC); 
  
  Serial.println(F(""));   
  Serial.print(F("#define RAW_BIT_MARK    "));
  Serial.print(IR_RAW.RAW_BIT_MARK, DEC);
  Serial.print(F("     // "));
  Serial.print(Probability[0] * USECPERTICK, DEC);
  Serial.print(F(" -> "));
  Serial.print(Temporary[0] , DEC);
  Serial.print(F(" times.  ")); 
  Serial.print(Probability[1] * USECPERTICK, DEC);
  Serial.print(F(" -> "));
  Serial.print(Temporary[1] , DEC);
  Serial.print(F(" times.  "));
  Serial.print(Probability[2] * USECPERTICK, DEC);
  Serial.print(F(" -> "));
  Serial.print(Temporary[2] , DEC);
  Serial.print(F(" times."));   

  Serial.println(F("")); 
  Serial.print(F("#define RAW_ONE_SPACE   "));
  Serial.print(IR_RAW.RAW_ONE_SPACE, DEC);
  Serial.print(F("     // "));
  Serial.print(Probability[0] * USECPERTICK, DEC);
  Serial.print(F(" -> "));
  Serial.print(Temporary[0] , DEC);
  Serial.print(F(" times. ")); 
  Serial.print(Probability[1] * USECPERTICK, DEC);
  Serial.print(F(" -> "));
  Serial.print(Temporary[1] , DEC);
  Serial.print(F(" times. "));
  Serial.print(Probability[2] * USECPERTICK, DEC);
  Serial.print(F(" -> "));
  Serial.print(Temporary[2] , DEC);
  Serial.print(F(" times."));  

  Serial.println(F("")); 
  Serial.print(F("#define RAW_ZERO_SPACE  "));
  Serial.print(IR_RAW.RAW_ZERO_SPACE, DEC);
  Serial.print(F("     // "));
  Serial.print(Probability[0] * USECPERTICK, DEC);
  Serial.print(F(" -> "));
  Serial.print(Temporary[0] , DEC);
  Serial.print(F(" times. ")); 
  Serial.print(Probability[1] * USECPERTICK, DEC);
  Serial.print(F(" -> "));
  Serial.print(Temporary[1] , DEC);
  Serial.print(F(" times. "));
  Serial.print(Probability[2] * USECPERTICK, DEC);
  Serial.print(F(" -> "));
  Serial.print(Temporary[2] , DEC);
  Serial.print(F(" times."));  
  Serial.println(F(""));

  Serial.println(F(""));  
  Serial.print(F("int rawData_Bits = ")); 
  Serial.print(IR_RAW.rawData_Bits, DEC); 
  Serial.print(F(" ;"));   
  Serial.println(F(""));   
  Serial.print(F("byte rawData["));
  Serial.print(IR_RAW.rawData_Count, DEC);
  Serial.print(F("] = { "));

  for (byte i = 0;  i < IR_RAW.rawData_Count;  i++) {
    Serial.print(F("0x"));
    Serial.print(IR_RAW.rawData[i], HEX);
    if ( i < IR_RAW.rawData_Count-1 ) Serial.print(F(", "));
  }
  Serial.println(F(" };"));

  Serial.print(F("unsigned int rawHole["));
  Serial.print(IR_RAW.rawHole_Count, DEC);
  Serial.print(F("][3] = { "));

  for (byte i = 0;  i < IR_RAW.rawHole_Count;  i++) {
    Serial.print(F("{ "));
    for (byte j = 0;  j < 3;  j++) { 
      Serial.print(IR_RAW.rawHole[i][j], DEC); 
      if ( j < 2 ) Serial.print(F(", ")); 
    }
    Serial.print(F(" } "));
    if ( i < IR_RAW.rawHole_Count-1 ) Serial.print(F(", "));
  }
  Serial.println(F("};"));
  Serial.println(F(""));
    
  Serial.println(F("// My Data End --------------- By MGP."));  
  Serial.println(F(""));
  Serial.println(F(""));  
}


//------- 以下為原紅外線程式庫範例內函數
void  encoding (decode_results *results)
{
  switch (results->decode_type) {
    default:
    case UNKNOWN:      Serial.print(F("UNKNOWN"));       break ;
    case NEC:          Serial.print(F("NEC"));           break ;
    case SONY:         Serial.print(F("SONY"));          break ;
    case RC5:          Serial.print(F("RC5"));           break ;
    case RC6:          Serial.print(F("RC6"));           break ;
    case DISH:         Serial.print(F("DISH"));          break ;
    case SHARP:        Serial.print(F("SHARP"));         break ;
    case JVC:          Serial.print(F("JVC"));           break ;
    case SANYO:        Serial.print(F("SANYO"));         break ;
    case MITSUBISHI:   Serial.print(F("MITSUBISHI"));    break ;
    case SAMSUNG:      Serial.print(F("SAMSUNG"));       break ;
    case LG:           Serial.print(F("LG"));            break ;
    case WHYNTER:      Serial.print(F("WHYNTER"));       break ;
    case AIWA_RC_T501: Serial.print(F("AIWA_RC_T501"));  break ;
    case PANASONIC:    Serial.print(F("PANASONIC"));     break ;
    case DENON:        Serial.print(F("Denon"));         break ;
  }
}


void  ircode (decode_results *results)
{
  // Panasonic has an Address
  if (results->decode_type == PANASONIC) {
    Serial.print(results->address, HEX);
    Serial.print(F(":"));
  }

  // Print Code
  Serial.print(results->value, HEX);
}


void  dumpInfo(decode_results *results)
{
  // Check if the buffer overflowed
  if (results->overflow) {
    Serial.println(F("IR code too long. Edit IRremoteInt.h and increase RAWBUF"));
    return;
  }

  // Show Encoding standard
  Serial.print(F("Encoding  : "));
  encoding(results);
  Serial.println(F(""));

  // Show Code & length
  Serial.print(F("Code      : "));
  ircode(results);
  Serial.print(F(" ("));
  Serial.print(results->bits, DEC);
  Serial.println(F(" bits)"));
}


//+=============================================================================
// Dump out the decode_results structure.
//
void  dumpRaw (decode_results *results)
{
  // Print Raw data
  Serial.print("Timing[");
  Serial.print(results->rawlen-1, DEC);
  Serial.println("]: ");

  for (int i = 1;  i < results->rawlen;  i++) {
    unsigned long  x = results->rawbuf[i] * USECPERTICK;
    if (!(i & 1)) {  // even
      Serial.print("-");
      if (x < 1000)  Serial.print(" ") ;
      if (x < 100)   Serial.print(" ") ;
      Serial.print(x, DEC);
    } else {  // odd
      Serial.print("     ");
      Serial.print("+");
      if (x < 1000)  Serial.print(" ") ;
      if (x < 100)   Serial.print(" ") ;
      Serial.print(x, DEC);
      if (i < results->rawlen-1) Serial.print(", "); //',' not needed for last one
    }
    if (!(i % 8))  Serial.println("");
  }
  Serial.println("");                    // Newline
}


2019.8.14 補充
使用於本製作內容的小米萬能遙控器是一代,後續有二代增強版、搭配小愛同學的版本等,介面會有所不同,若無創維機上盒的選項,可用長虹電視代替,可將 45-55 行替換如下,再上傳至晶片
#define N00  0xBD827D   //數字盤 0
#define N01  0xBDA857
#define N02  0xBD6897
#define N03  0xBDE817
#define N04  0xBD9867
#define N05  0xBD58A7
#define N06  0xBDD827
#define N07  0xBDB847
#define N08  0xBD7887
#define N09  0xBDF807
#define N10  0xBD50AF   //數字盤 確定

或者可使用其它電視(請先將 實體小米萬能遙控器  對準 轉譯器接收模組)
    A. 請將上方程式 24-25 行左方的雙斜線拿掉,將程式上傳至晶片進入除錯模式
    B. 選取米家APP內萬能遙控器中任一牌電視測試(按任一鍵均可),若此電視遙控器是NEC編碼的,在Arduino IDE監控視窗內會出現編碼代號,若無請更換其它牌電視繼續測試
    C. 找到的話請依序記錄 "0~9 確定" 共11個鍵的NEC編碼代號替換 45-55 行編碼代號 (0x 之後的16進位數字)
    D. 加回 24-25 行的雙斜線,將程式上傳至晶片即完成

03設定篇 請改用新的電視數字鍵盤操作即可。

積木式免焊接 記憶型空調紅外線轉譯器 - 03 設定 記憶型空調紅外線轉譯器

< 本文請勿轉載,謝謝 ! >

設定 記憶型空調紅外線轉譯器


添加 米家APP遙控器,作為和轉譯器中繼使用

以下使用實體小米萬能遙控器一代
打開手機(Android)  米家APP -> 萬能遙控器 -> 添加遙控器 -> 點選機頂盒 -> 點選衛星/機頂盒 -> 點選 創維 Skyworth (NEC編碼)




接著請依螢幕中的按鍵測試,均選"有響應"以完成建立步驟 -> 點選 數字 -> 出現數字鍵盤

錄製自家冷氣空調紅外線編碼

請將實體小米萬能遙控器對準轉譯器的接收模組
1. 按下上方圖內創維數字鍵盤的右下"確定",會聽見 長嗶3聲,進入設定狀態
2. 按下上方圖內創維數字鍵盤的 0~9 任一個數字鍵,會聽見對應的嗶聲
3. 將家中的空調遙控器對準轉譯器的接收模組按下所需按鍵,若聽見 短嗶2聲 即完成錄製,若無聲表示錄製失敗。
    ( 若按錯鍵可重按,只保留最後錄製的信號 )
4. 初次使用時請重覆上 2-3 步驟錄製3個數字做測試即可。
5. 再按下上方圖內創維數字鍵盤的右下"確定",會聽見 長嗶4聲,結束設定狀態

測試錄製信號

初次測試時,第1點非常重要,紅外線是指向性的,沒對準是不能用的。
1. 請儘量將轉譯器發射模組的LED的正上方圓頂接近並對準冷氣機紅外線接收器的位置
2. 轉譯器能確實收到實體小米萬能遙控器的信號
3. 再按下在上述設定步驟中有使用到的3個 米家APP創維遙控器的 0-9 數字鍵,看結果如何

若有1個以上成功的話表示轉譯器應可適用,失敗的 自家紅外線設定 紀錄再錄製一次可能就可用,我錄製信號的成功率保守估記是 80%。

接著錄製測試完全部所需的設定,再來擺放調整自家適合的位置角度,測試定位後的轉譯器接收發射都正常。

測試完成後就不需要再用到手機去操作了,日後操作使用的方式為:
小愛同學 (或遠端操作) -> 連動實體小米萬用遙控器 -> 透過此電路轉譯成自家冷氣紅外線信號發送 -> 讓自家冷氣空調動作

剩下的就是依各人需求將這個加入米家APP的智慧連動了,希望你也能順利補完這塊拼圖。



PS:若網友完成可用的話,煩請將 冷氣機、遙控器型號 回報在留言板上,我再彙整提供其他網友參考。



註:若不順利3組錄製信號都不行的話,簡易檢查如下:
1. 按下 米家APP創維遙控器的 0-9 數字鍵 時,轉譯器的接收模組無閃燈
A:實體小米萬能遙控器沒對準轉譯器。

2. 轉譯器發射信號時,發射模組有無閃燈
A:發射模組接線錯誤,或尚未錄製信號供發射

3. 轉譯器發射模組的LED的正上方圓頂有對準冷氣機紅外線接收器的位置
A:紅外線是指向性的,我初次測試時也以為失敗了,後來才發現紅外線發射模組無輔助聚光處(如自家遙控器發射端有凹陷處),所以角度非常重要,初次測試時請儘量接近冷氣機紅外線接收處,可用後再拉長距離調整角度。

若再三檢查都不行的話,則要進入程式的除錯模式,查看接收到的紅外線信號是如何,各家紅外線編碼不同,再客製修正程式調整。

最後是 紀錄篇,紀錄網友的回報狀況及修正。


積木式免焊接 記憶型空調紅外線轉譯器 - 04 紀錄 & 修正篇
Xiaomi Mijia universal infrared ray remote control for Unsupported air conditioner.

積木式免焊接 記憶型空調紅外線轉譯器 - 02 軟體 程式寫入篇

< 本文請勿轉載,謝謝 ! >

軟體 程式寫入篇


Arduino 開發程式及紅外線程式庫安裝

首先安裝Arduino 開發板的編寫程式,請至 Arduino官網(如下連結)下載 IDE開發環境 主程式
如我的電腦是 Windows 10,下載 Windows Installer, for Windows XP and up 版本
https://www.arduino.cc/en/Main/Software


下載時會詢問是否要贊助,點選下方 JUST DOWNLOAD 可免費下載主程式。


下載完畢後執行EXE檔主程式安裝,一步一步如下圖即可完成:
待 Completed 出現後,Close 關閉安裝程式,Arduino IDE開發環境 程式即安裝完成。


接下來安裝紅外線程式庫 (IR remote by shirriff)

請打開桌面上安裝好的 Arduino 圖式程式,一步一步如下圖即可完成:

安裝好的 紅外線程式庫 儲存在使用者的「文件」目錄下。


Arduino 開發版晶片程式寫入 (程式與 擴展版 通用)

0. 請先關閉任何開啟的 Arduino IDE開發環境程式
1. 將Arduino 開發板的USB連接線插入電腦任一 USB 2.0 埠上進行供電及準備傳輸。
2. 請下載以下壓縮檔 (解壓縮密碼藍字 mygraphpaper.com 解壓縮

https://www.mygraphpaper.com/download/MPG_TR_Translator.rar

3. 請將檔案內的 IRremoteInt.h、IRremote.cpp 複製至如上圖 使用者「文件」下的Arduino\libraries\IRremote\ 目錄內,要覆蓋原本檔案。
4. 雙擊打開下載檔案內 MPG_IR_Translator 目錄下的 MPG_IR_Translator.ino 程式檔
5. 準備上傳晶片程式:Arduino IDE應用程式開啟後,請調整一下偏好設定


接著確認開發板的型號選至 Arduino/Genuino Uno

接著選擇序列埠 COMX (Arduino/Genuino Uno),COM埠的編號每台電腦皆不同,確認括號內的型號即可。

接著打開序列埠監控視窗,這個視窗可監控Audrino開發板回傳的訊息,是非常常用的視窗。

執行 "驗證/編譯" 本程式,結果應如下圖,請注意Arduino IDE下方黑色顯示視窗有無出現其它錯誤訊息 (有出現 可用記憶體低下 的警告訊息表示上方 步驟3 的檔案才有覆蓋成功)

接著請執行"上傳",將程式寫入Arduino 開發板晶片內。上傳後請等Arduino IDE下方黑色顯示視窗出現如下圖示,請注意驗證是否成功。(若出現 Verification error 表示這張 Arduion開發板的晶片有問題,需向店家更換良品)

上傳成功後:
1. 蜂鳴器會 Beep 一聲 (蜂鳴器上的貼紙請勿撕掉,若撕掉會變很大聲)
2. 監控視窗會出現:MPG IR-Translator is Ready.

軟體部份完成,接下來進行設定部份。


積木式免焊接 記憶型空調紅外線轉譯器 - 03 設定 記憶型空調紅外線轉譯器