Prism

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 設定 記憶型空調紅外線轉譯器

積木式免焊接 記憶型空調紅外線轉譯器 - 01 硬體 電子積木篇

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

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


本節文章將以如何快速實作可使用的空調紅外線轉譯器為主,較細節理論的部份就不多詳述,若有興趣的話可參考之前的實驗紀錄「冷氣機紅外線遙控訊號連動」。

硬體 電子積木篇

硬體使用如下的電子積木方式製作的話可完全不用焊接,只需 4個零件、7條線連接,再整線一下就可以了。


所需硬體材料

為方便採買,直接複製網拍賣家的商品名稱(本站不販賣零件,也沒有合作的店家,請自行採購)
1. 紅外線發射模塊 紅外線 發射 模塊 模組 發射傳感器模塊 小車 電子積木  (請注意模組上要有焊 黑色電阻 的)
2. 紅外線傳感器接收模組 1838
3. Arduino 全相容 UNO R3 開發板 ATMega16U2【送USB線】
4. 蜂鳴器 有源蜂鳴器 5V 採用SOT塑封管 電磁式蜂鳴器                     (規格為 有源)
5. 彩色排線 連接線 杜邦線 10公分 公對公 公對母 母對母 10條1單位  (規格為 公對母)
6. 保護殼-UNO R3 開發版壓克力外殼 Arduino 全相容                       (新版的外款不用螺絲)

全部材料清單及價格如下,請注意以下紅色所指規格,不要買錯了。(2019年8月)


接線圖如下:
總共零件材料如下,拿到時請先檢查所有零件外觀有無問題,再將Arduino 開發版接上 5V USB充電器,確認電源燈有正常亮起。(實驗時千萬不要用行動電源供電,以免零件短路造成行動電源電池爆炸危險)


接線步驟: (以下接線前請確認 Arduino 開發版 沒有 接上任何電源)
PS:杜邦接線母頭的那方若多次插拔易鬆動不牢,要整線請拔公頭(和開發板連接那邊)。
1. 將 Arduino 開發版放進壓克力保護殼內,蓋回上殼
2. 參照上方接線圖連接 紅外線接收模組 - 3條線
3. 參照上方接線圖連接 紅外線發射模組 - 2條線
4. 參照上方接線圖連接 有源蜂鳴器 - 2條線
5. 整理一下接線,可用薄雙面膠或膠帶暫固定 (請依放置需要,調整模組左右邊,但請注意接收模組要背對發射模組)


最後請再確認一次所有針腳有無鬆掉及接對位置,若接法有疑問請先確認,不要冒然送電以免零件燒毀。

沒問題後請將 Arduino 開發版接上 5V USB充電器,確認電源燈有正常亮起,各零件沒有異樣即可,硬體部份即完成,接下來進行軟體部份。


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

2019年7月22日 星期一

生活小物-B7000 萬用膠 (米動手錶 螢幕貼合)

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

B-7000 萬用膠 (米動手錶 螢幕貼合)


一年半前幫家人買的 AMAZFIT 米動手錶 青春版,年初左右拿給我看說螢幕翻開脫落了,看似脫膠,還好螢幕有接著一條排線才沒遺失,而這個時間點已經過一年保固了,看看手錶還能正常運行,只好自己想辦法修了,上網查了一下,這種 B-7000的多功能膠可以貼合螢幕,就買個 15ml 來試用了 (不到台幣 50元)。

處理的步驟如下 (當時忘了要拍圖了,可參考 Mobile01網友的圖 ):
1. 將原手錶上四周及錶蓋上的殘膠清除乾淨 (可用 鑷子、刀片、酒精)
2. 均勻的在錶體上方四周圍內抹上一圈 B-7000 膠水,我是怕漏水上了二圈,再檢查有沒有沒塗到的死角
3. 錶蓋對準壓回錶座
4. 在錶蓋上方放個重物壓了一天
5. 等膠乾了後,錶蓋上會有溢膠,透明軟軟的,可拿個尖鑷子清除大部份,再用酒精擦拭乾淨即可。

用到現在半年多了沒有什麼問題,手錶是拿來用的,生活的痕跡難免就是了。

註:這瓶萬用膠開封後,就算有蓋緊,過一段時間還是會全部硬掉,建議買夠用的大小就好





2019年7月18日 星期四

小米空氣淨化器當精油擴香機

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

小米空氣淨化器當精油擴香機


當初購買 小米空氣淨化器 2S,看到正上方那圓圓的位置時,我就決定了,它要當房間內的大型精油擴香器~~

之前想買無印良品的香氛機,但家中溼度經常在70左右,再用水霧的話就要霉到家了。
網上有各式各樣的 香薰儀 擴香機 芳香噴霧 精油擴香器等,這個替代方式好不好就看看吧。

上網拍找了好久,沒有看到合適搭配的精油擴香用品,只好轉到日本的 Amazon 去看看,終於發現好像合適的東西了,共買了下列三個試試看:

価格: ¥389 



第一種是霧狀的半透明塑料盤子,質料還好,有附方型的上下蓋,不用的時候可以蓋起來或半蓋,減少精油味道散出或積塵,盤子表面有細細的紋路,看說明是類似毛細現象幫助精油揮發。這個睡覺的時候放在旁邊,效果還不錯,偶然傳來悠悠的香味~~

第二種和第三種是相同的,只是印刷不同,是白色的陶器,看說明是陶器內有細微的小孔,可加大面積幫助精油揮發。



後來考慮有時候會更換不同的精油,第一種很方便清洗,而第二種跟第三種就先收起來了,如果是使用固定精油的話,第二種和第三種會有點像在養陶器的感覺,久了之後,應該會有不同的感受。

若想試試看的話,家中沾醬的小圓盤可以拿來用看看,不一定要買上面的擴香盤。

滴上3滴檸檬複合精油,放上清淨機上的圓座,配合微風,不用多久房內就充滿了檸檬清新微甜的味道,要放鬆心情的時候很好用,可維持個 1 ~ 2 小時左右~~

若像是覺得空氣有異味、有客人要來家裏的時候,這個擴香的效果很快,算滿實用的。

小提醒一下,可在擴香盤下貼個薄的雙面膠帶固定可避免盤子走位或打翻。

使用小米智能插座開啟公寓一樓大門

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

使用「米家智能插座」開啟公寓一樓大門


※ 以下操作,無電工經驗者需請有經驗的朋友幫忙協助

前陣子朋友家中的公寓大門對講機故障了找我去看看,拆下對講機檢查線路已經老舊,且機型簡單建議可自己換一台 (俞氏牌 室內對講機 LT-380A),而對講機的下方有設插座,朋友家中也有裝設小米物聯網的設備,在這個地利的環境下,我就問要不要順便用米家APP去開啟樓下大門呢?

當初的構想是 :
  1. 遠端操作米家智慧插座打開電源
  2. 啟動計時電路及繼電器
  3. 透過繼電器模擬按下對講機的開門鍵
  4. 一秒左右後電路自動關閉繼電器
  5. 米家APP再關閉小米智能插座

先 關閉電路的繼電器 再 關閉小米插座 是為了保險起見,避免不要意外的一直開啟一樓的門。

本想DIY製作簡單的計時電路,在找零件的時候,發現已有現成經濟的模組可用(台幣 100元左右),只要小改一下就符合需求了,借網拍模組照片如下 ( 延時繼電器模組 ):

網拍賣家在商品說明中有詳細的參數資料,主要使用 NE555計時器 + 繼電器
1. 下方 S1 是一個小按鈕,為低電位觸發開關,S1 右方接點與 電路板左側 V- 腳位相通,均接地
2. S1 左方接點與 電路板左側 TRIG 腳位相通,再連接 R2電阻 2.4K歐姆,再接電源

使用時在電路板左側焊上一個 100u的電解電容模擬一送電時即做低電位觸發:
1. 接腳 TRIG  接電解電容+極
2. 接腳 V-       接電解電容-極
電容正負、極接反會有危險,電容會爆掉的,請特別注意,且耐壓最好選電源的2倍以上,這邊電源使用USB 5V,所以最好使用耐電壓10伏特以上。我手邊現有的為耐壓 25 伏特,就直接拿來用了。

使用電源 5V,電阻 2.4K歐姆,電容 100u,根據RC充電計算約送電 0.24 秒後會自 低電位 到達 高電位,下方網站有 電阻電容 (RC) 時間常數計算器 ,可套用數值去概算。
https://www.digikey.tw

利用電路上方藍色可變電阻的旋鈕,可以調整繼電器開啟時間,我是調為1秒左右(可按下 S1 開關去測試),電路動作為:
1. Micro USB 5V供電  一送電時電路因電容低電位觸發,而啟動繼電器:右側 下方(常開端NO)、中間(公共端COM) 會短路相通
2. 一秒後,NE555計時的時間到 ,電路關閉繼電器:右方 下方(常開端NO)、中間(公共端COM) 會斷開不通

再設定米家APP智慧連動 (小米插座 Turn on 後延遲的時間不要設太短,有時小米插座啟動會有時間差)

設定好後,用麵包板接上LED模擬繼電器導通狀態:


小米插座斷電後,電路上的 100u 電容約 3 秒左右才會放完電,也就是說再次使用請在 3 秒以後,如果使用的電容大小不同,可重新使用的時間也會不同。

模組測試好後,從對講機的開門按鈕開關二端接點,焊接拉二條單芯線(不用太粗)鎖到模組的繼電器中:下方(常開端NO)、中間(公共端COM)

電鎖開門的電路動作為 ( 以下顏色不一定與自家接線顏色相同,請留意)
1. 電從電源供應器(棕 A, AC 24V)送出,到達電鎖 A 點
2. 電鎖內 A、EL 間有電磁鐵線圈,導通時會拉入門栓開門
3. 電鎖 EL 線路連接至自家對講機 (紅 EL1) 接點
4. 自家對講機按下 開門按鈕 的話,(紅 EL1) 和 (黑 -) 會短路導通,導通後會形成電路迴圈
5. 自家對講機的 (黑 -) 與電源供應器的地線 (黑 -) 連接,走完電流迴圈
6. 電鎖開門後,門栓已彈出,此時電鎖內 A 和 PT 會導通,自家對講機的 開門指示燈 就會亮起

整理一下牆壁佈線即完成 (我是把這個模組塞進對講機的牆壁洞中,再拿一條夠長的 Micro USB 線接到小米智能插座上的 5V USB 手機充電器上),朋友家中已弄好一段時間了,不好再去拍照,何況還要拆開對講機啊...

之後可將米家APP智慧設定建一個捷徑到手機桌面,這樣不用開米家APP只要按一下捷徑圖示就能開門,當然小愛同學也能幫忙。

這種透過繼電器隔離兩方電源,模擬"按一下"開關的動作,有些時候會有需要 (請注意不要用在可能造成危險的地方,如鐵捲門),利用這種方式也可將某些電器控制加入自家的物聯網之中。

2019年7月16日 星期二

冷氣機紅外線遙控訊號連動 - 06 紅外線訊號轉譯程式

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

※ 2019.8 後補「積木式免焊接 記憶型空調紅外線轉譯器」一文,可供快速製作成品

紅外線訊號轉譯程式


程式製作流程概要:

1. 錄製米家APP內小米萬能遙控器內建的任一個NEC編碼遙控器 0~9 數字鍵的紅外線編碼
2. 錄製自家冷氣空調的 10組常用信號,對應上 1 的 0~9 數字鍵,10組的話日常使用應足夠
3. 程式基本流程為
    A. 讀取紅外線接收器的編碼信號
    B. 比對符合電視遙控器數字鍵(上 1 )中的那個數字
    C. 發射與數字對應的自家空調紅外線錄製信號(上 2)
    D. 過程中 LED 、蜂鳴器會有閃光及聲音作提示


錄製米家APP內小米萬用遙控器的 0~9 數字鍵的紅外線編碼

請先將前篇 04中修改好的 IRrecvDumpV2 程式編譯上傳至 Arduino 開發板內,並將實體的小米萬能遙控器對準電路板上的紅外線接收器,準備接收記錄信號。

PS: 購買的實體小米萬能遙控器請先確認已完成綁定米家APP ( 以下使用小米萬能遙控器一代 )
打開手機中米家APP內的萬能遙控器圖示,選擇 添加遙控器 -> 點選機頂盒 -> 點選衛星/機頂盒 -> 點選 創維 Skyworth (隨機挑選的NEC編碼遙控器,配合下方紅外線轉譯程式使用NEC 編碼判別,若要用別的遙控編碼協定請再自行修改程式碼) 


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


請依序按下數字鍵的 0~9 ,並複製紀錄下各數字的紅外線編碼,如數字 0,對應的NEC編碼如下 0x80BFE11E (0x 表示16進位)

創維機頂盒 遙控器數字 0~9 的NEC編碼紀錄如下:
0     0x80BFE11E
1     0x80BF49B6
2     0x80BFC936
3     0x80BF33CC
4     0x80BF718E
5     0x80BFF10E
6     0x80BF13EC
7     0x80BF51AE
8     0x80BFD12E
9     0x80BF23DC


錄製自家冷氣空調常用設定的紅外線編碼

請先構思自家空調常用的設定有那些,將之編號為 0~9 (沒那麼多組也沒關係,日後可再添加修改),舉例如下:
0     關機 (不用考慮其它參數設定) 
1     保留
2     保留
3     保留
4     保留
5     快速冷房:開機 , 冷氣 , 25度 , 風速4級(最大) , 風向自動
6     一般使用:開機 , 冷氣 , 26度 , 風速自動, 風向自動
7     一般使用:開機 , 冷氣 , 27度 , 風速自動, 風向自動
8     一般使用:開機 , 冷氣 , 28度 , 風速自動, 風向自動
9     搭配風扇:開機 , 冷氣 , 29度 , 風速自動, 風向自動

請按出構思的空調設定,並同上方式複製紀錄下紅外線編碼參數,如 關機,對應的編碼資料如下

錄製好所有的編碼後,可能發現每個設定編碼其下方藍色參數部份可能會不盡相同,可以直接套用任何一組使用,應該都還在取樣誤差範圍內,或可以取出現最多次的數值作為較佳參數使用。
下方的 byte rawData 陣列,在其上方一行請手動為其加上設定的註解以方便日後判讀了解( // 後為註解符號,不會被編譯),並為每個 rawData 陣列加上數字編號以供程式識別 (紅色部份),全部取樣整理如下:

#define RAW_HDR_MARK    3350
#define RAW_HDR_SPACE   1800
#define RAW_BIT_MARK       400     // 450 -> 17 times.  400 -> 66 times.  350 -> 55 times.
#define RAW_ONE_SPACE   1350     // 1350 -> 11 times. 1300 -> 11 times. 500 -> 65 times.
#define RAW_ZERO_SPACE  500     // 500 -> 65 times. 450 -> 40 times. 400 -> 2 times.
#define RAW_RPT_MARK     400      //Repeat Gap
#define RAW_RPT_SPACE    16950

int rawData_Bits = 144 ;

//0     關機 (不用考慮其它參數設定) 
byte rawData_0[18] = { 0xC4, 0xD3, 0x64, 0x80, 0x0, 0x0, 0x18, 0xB0, 0x6C, 0x2, 0x6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8 };

//5     快速冷房:開機 , 冷氣 , 25度 , 風速4級(最大) , 風向自動
byte rawData_5[18] = { 0xC4, 0xD3, 0x64, 0x80, 0x0, 0x4, 0x18, 0x90, 0x6C, 0x22, 0x46, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4C };

//6     一般使用:開機 , 冷氣 , 26度 , 風速自動, 風向自動
byte rawData_6[18] = { 0xC4, 0xD3, 0x64, 0x80, 0x0, 0x4, 0x18, 0x50, 0x6C, 0x2, 0x46, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xF4 };

//7     一般使用:開機 , 冷氣 , 27度 , 風速自動, 風向自動
byte rawData_7[18] = { 0xC4, 0xD3, 0x64, 0x80, 0x0, 0x4, 0x18, 0xD0, 0x6C, 0x2, 0x46, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC };

//8     一般使用:開機 , 冷氣 , 28度 , 風速自動, 風向自動
byte rawData_8[18] = { 0xC4, 0xD3, 0x64, 0x80, 0x0, 0x4, 0x18, 0x30, 0x6C, 0x2, 0x46, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8C };

//9     搭配風扇:開機 , 冷氣 , 29度 , 風速自動, 風向自動
byte rawData_9[18] = { 0xC4, 0xD3, 0x64, 0x80, 0x0, 0x4, 0x18, 0xB0, 0x6C, 0x2, 0x46, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4C };


完成紅外線轉譯程式

請注意執行本程式前
1. 需將前篇03、04中修改的 IRremoteInt.h 檔案內的 RAWBUF、_GAP 二個參數改回預設值,以維持程式穩定:
    #define RAWBUF  101
    #define _GAP    5000

2. 再來貼上對應的創維遙控器編碼
N00~N09必須有值,沒有的部份請補 0,若使用相同的遙控器則不用修改
#define N00     0x80BFE11E
#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

3. 貼上自家的冷氣空調錄製編碼
    A. 從第35行 #define RAW_HDR_MARK 行數開始對應貼上
    B. rawData_0[XX] ~ rawData_9[XX] 必須有值,沒有的部份請改成
        byte rawData_X[1] = { 0 };      //X為對應的數字, 可參考 48 ~ 58 行中的語法

其它就不用修改了,測試正常後可再依自己的需求去修改程式內容,上面已有提過程式基本流程為
    A. 讀取紅外線接收器的編碼信號
    B. 比對符合電視遙控器數字鍵中的那個數字
    C. 發射與數字對應的自家空調紅外線錄製信號
    D. 過程中 LED 、蜂鳴器會有閃光及聲音作提示

完成的程式碼如下:
/*
   IRremote
   An IR LED must be connected to Arduino PWM pin 3.
   Copyright Ken Shirriff
   http://arcfn.com

   2019.7.16 By MGP.
   Only for Air conditioner.  https://www.mygraphpaper.com blog.
*/

#include <IRremote.h>

//請注意執行本程式前, 需將 IRremoteInt.h 內 RAWBUF, _GAP 改回預設值
//IRremoteInt.h
//#define RAWBUF  101
//#define _GAP    5000


//創維機頂盒 遙控器數字 0~9 的NEC編碼
//N00~N09必須有值,沒有的請補 0
#define N00     0x80BFE11E
#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


//自家的冷氣空調編碼參數
//所有的 rawData_X 陣列必須有值,沒有的請補 byte rawData_X[1] = { 0 }; //(X為對應的數字)
#define RAW_HDR_MARK    3350
#define RAW_HDR_SPACE   1800
#define RAW_BIT_MARK    400     // 450 -> 17 times.  400 -> 66 times.  350 -> 55 times.
#define RAW_ONE_SPACE   1350     // 1350 -> 11 times. 1300 -> 11 times. 500 -> 65 times.
#define RAW_ZERO_SPACE  500     // 500 -> 65 times. 450 -> 40 times. 400 -> 2 times.
#define RAW_RPT_MARK    400      //Repeat Gap
#define RAW_RPT_SPACE   16950

int rawData_Bits = 144 ;

//0     關機 (不用考慮其它參數設定) 
byte rawData_0[18] = { 0xC4, 0xD3, 0x64, 0x80, 0x0, 0x0, 0x18, 0xB0, 0x6C, 0x2, 0x6, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8 };

//1     保留 不使用的數字請用下行宣告做預設值
byte rawData_1[1] = { 0 };

//2     保留 不使用的數字請用下行宣告做預設值
byte rawData_2[1] = { 0 };

//3     保留 不使用的數字請用下行宣告做預設值
byte rawData_3[1] = { 0 };

//4     保留 不使用的數字請用下行宣告做預設值
byte rawData_4[1] = { 0 };

//5     快速冷房:開機 , 冷氣 , 25度 , 風速4級(最大) , 風向自動
byte rawData_5[18] = { 0xC4, 0xD3, 0x64, 0x80, 0x0, 0x4, 0x18, 0x90, 0x6C, 0x22, 0x46, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4C };

//6     一般使用:開機 , 冷氣 , 26度 , 風速自動, 風向自動
byte rawData_6[18] = { 0xC4, 0xD3, 0x64, 0x80, 0x0, 0x4, 0x18, 0x50, 0x6C, 0x2, 0x46, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xF4 };

//7     一般使用:開機 , 冷氣 , 27度 , 風速自動, 風向自動
byte rawData_7[18] = { 0xC4, 0xD3, 0x64, 0x80, 0x0, 0x4, 0x18, 0xD0, 0x6C, 0x2, 0x46, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xC };

//8     一般使用:開機 , 冷氣 , 28度 , 風速自動, 風向自動
byte rawData_8[18] = { 0xC4, 0xD3, 0x64, 0x80, 0x0, 0x4, 0x18, 0x30, 0x6C, 0x2, 0x46, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x8C };

//9     搭配風扇:開機 , 冷氣 , 29度 , 風速自動, 風向自動
byte rawData_9[18] = { 0xC4, 0xD3, 0x64, 0x80, 0x0, 0x4, 0x18, 0xB0, 0x6C, 0x2, 0x46, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x4C };

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


#define IR_NUM_SIZE  10             //使用0-9,共10個按鍵

int recvPin = 2;                    //紅外線接收在 Pin2; 發射內定在 Pin3
//int Pin9_SW = 9;                  //Pin9 按鈕保留
int Recv_Enable_LED_Pin = 10;       //Pin10 啟動指示燈
int LED_Buzzer_Pin = 11;            //Pin11 動作指示燈及聲音
int khz = 38;                       //使用 38kHz 頻率發射

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

uint8_t IR_MAP_Index;               //儲存找到對應的 0-9 數字
struct NUM_MAP {                    //將0-9的紅外線編碼對應成數字 0-9 ,方便比對搜尋
  unsigned long IR_CODE;
  uint8_t INDEX_NUM;
};
static 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}};


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


//發送紅外線編碼陣列, 位元由左至右發送, 資料共計 rawData_Bits 位元
//void RAWIRSend(發送陣列資料, 陣列大小, 發射位元數, 發射頻率)
void RAWIRSend(byte SendRAWData[], unsigned int SendRAWData_Len, unsigned int SendRAWData_Bits, unsigned int hz)
{
  int Rep_Temp = 0; 
  unsigned int rawData_Bits_Count =0;
  byte Mask = 1; 

  if (RAW_RPT_SPACE > 0) Rep_Temp = 2;
  else Rep_Temp = 1;
    
  irsend.enableIROut(hz);  // khz
  irsend.space(max(RAW_HDR_MARK,RAW_HDR_SPACE));  //Initial signal
  
  for (int j = 0; j < Rep_Temp; j++) {  // if Repeat
    //Start Single
    irsend.mark(RAW_HDR_MARK);
    irsend.space(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 (SendRAWData[i] & Mask) {
            //Bit 1
            irsend.mark(RAW_BIT_MARK);
            irsend.space(RAW_ONE_SPACE);
          }
          else {
            //Bit 0
            irsend.mark(RAW_BIT_MARK);
            irsend.space(RAW_ZERO_SPACE);
          }
          rawData_Bits_Count++;
        }
      }
    }
    if (Rep_Temp > 1) {
      //Send Repeat Single
      irsend.mark(RAW_RPT_MARK);
      irsend.space(RAW_RPT_SPACE);      
    }
    irsend.space(max(RAW_HDR_MARK,RAW_HDR_SPACE));  //Stop Single
  }  
}

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

  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, HIGH);                 //打開 主板指示燈, 要關閉請改 LOW
}

//主程式迴圈
void loop() {
  if (irrecv.decode(&results)) {         //是否偵測到紅外線信號
    if (results.decode_type == NEC) {    //判斷為哪個數字鍵, 只偵測 NEC 編碼
      Serial.print("NEC Code : ");       //提供序列埠監看
      Serial.print(results.value, HEX);
      IR_MAP_Index = IR_NUM_SIZE;
      for (uint8_t i = 0; i < IR_NUM_SIZE; i++) {
        if (results.value == IR_MAP[i].IR_CODE) {
          IR_MAP_Index = IR_MAP[i].INDEX_NUM;
          break;
        }
      }
      Serial.print("  Map Index : ");
      Serial.println(IR_MAP_Index, DEC);
      
      if (IR_MAP_Index < IR_NUM_SIZE) { //是否有找到對應的數字鍵
        delay(100);                     //收發間隔,穩定用
        switch (IR_MAP_Index) {         //保留每個數字對應的程式操作空間
          case 0:
            ST_LED_Buzzer (10);     //蜂鳴器先叫, 以免和冷氣混在一起
            RAWIRSend(rawData_0, sizeof(rawData_0) / sizeof(rawData_0[0]), rawData_Bits, khz);
            break;
          case 1:
            ST_LED_Buzzer (1);     //蜂鳴器先叫, 以免和冷氣混在一起
            RAWIRSend(rawData_1, sizeof(rawData_1) / sizeof(rawData_1[0]), rawData_Bits, khz);
            break;
          case 2:
            ST_LED_Buzzer (2);     //蜂鳴器先叫, 以免和冷氣混在一起
            RAWIRSend(rawData_2, sizeof(rawData_2) / sizeof(rawData_2[0]), rawData_Bits, khz);
            break;
          case 3: 
            ST_LED_Buzzer (3);     //蜂鳴器先叫, 以免和冷氣混在一起
            RAWIRSend(rawData_3, sizeof(rawData_3) / sizeof(rawData_3[0]), rawData_Bits, khz);
            break;
          case 4:
            ST_LED_Buzzer (4);     //蜂鳴器先叫, 以免和冷氣混在一起
            RAWIRSend(rawData_4, sizeof(rawData_4) / sizeof(rawData_4[0]), rawData_Bits, khz);
            break;
          case 5:
            ST_LED_Buzzer (5);     //蜂鳴器先叫, 以免和冷氣混在一起
            RAWIRSend(rawData_5, sizeof(rawData_5) / sizeof(rawData_5[0]), rawData_Bits, khz);
            break;
          case 6:
            ST_LED_Buzzer (6);     //蜂鳴器先叫, 以免和冷氣混在一起
            RAWIRSend(rawData_6, sizeof(rawData_6) / sizeof(rawData_6[0]), rawData_Bits, khz);
            break;
          case 7:                          
            ST_LED_Buzzer (7);     //蜂鳴器先叫, 以免和冷氣混在一起
            RAWIRSend(rawData_7, sizeof(rawData_7) / sizeof(rawData_7[0]), rawData_Bits, khz);
            break;
          case 8:
            ST_LED_Buzzer (8);     //蜂鳴器先叫, 以免和冷氣混在一起
            RAWIRSend(rawData_8, sizeof(rawData_8) / sizeof(rawData_8[0]), rawData_Bits, khz);
            break;
          case 9:
            ST_LED_Buzzer (9);     //蜂鳴器先叫, 以免和冷氣混在一起
            RAWIRSend(rawData_9, sizeof(rawData_9) / sizeof(rawData_9[0]), rawData_Bits, khz);
            break;
        }
        irrecv.enableIRIn();    //IRRemote 在發射的時候會把接收功能停掉, 重開接收
      } else {
        irrecv.resume();    //沒收到定義的 0-9 信號, 重新接收
      }
    }
    irrecv.resume();    //沒收到NEC信號, 重新接收
  }
}

請將程式編譯上傳,再次提醒測試時請一定要將紅外線發射LED的正上方圓頂對準冷氣機紅外線接收器的位置,測試時按下上述中米家APP內創維遙控器的 0-9 數字鍵,順利的話冷氣機就會做出對應的動作了。

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

剩下的就是依各人需求將這個加入米家APP的智慧連動了,這樣小愛同學也能輕鬆幫忙。

想做這個東西很久了,現在終於完成,實在是說不出的高興,補完了心中的一塊拼圖。

謝謝耐心看完這份小專題。



Xiaomi Mijia universal infrared ray remote control for Unsupported air conditioner.