大き目ドットの有機EL, WS0010 で Yahoo ニュース リアルタイム 電光掲示板を作ってみた

記事公開日:2016年11月2日
最終修正日:2017年10月2日

スポンサーリンク

※ ESP-WROOM-02 の Flashメモリサイズが 4MB のはずが、実は 2MB だったという情報がありました。
以下のページを参照して、Flashメモリサイズを確認しておくことを強くお勧めします。

ESP-WROOM-02 ( ESP8266 ) チップ・メモリ・MACアドレス情報確認方法
(2017/10/2)

Yahoo!ニュース RSS サイトが https ( SSL )化されてしまいました。
よって、現在、電光掲示板に表示できなくなっています。
今、自作ライブラリ EasyWebSocket をSSL対応にしている途中です。
しばらくお待ち下さいませ。
ご不便をおかけします。
m(_ _)m
(2017/4/3時点)

こんばんは。

前回の記事に引き続き、今回は Yahoo! RSS のニューストピックス電光掲示板を作ってみました。
大き目ドットのキャラクタ&グラフィック有機EL WS0010 に表示させていて、ESPr Developer ( ESP-WROOM-02, ESP8266 ) とGPIO エキスパンダ MCP23S08 を使用しています。

以下の記事

Web News記事自動取得 OLED ( 有機EL )ミニ電光掲示板に16×16フリー日本語フォント( 東雲 ) を使う

でも同じようなことをやりましたが、今回は何といっても文字が大きいというところがミソです。16×16ドットの日本語漢字が高速スクロールできるというところが違います。

また、こちらの記事

自作LED電光掲示板に Yahooニュースや 天気予報 を表示させ、さらに NTP 時計機能追加しました

では、文字は大きくて高速スクロールしますが、8×8ドットなので、複雑な漢字は読み取りにくいものでした。

ということで、今回でこの Yahoo! RSS ニュース 電光掲示板は私にとってほぼ理想的なものとなりました。
ケースを加工すれば、デスクトップにどこでも設置できるWEB連携 電光掲示板 として、とてもイイ具合ではないでしょうか。

ということで、まずは以下の動画をご覧ください。

いかがでしょうか。
NTP時刻自動補正、時計表示もあります。
ちなみに、この動画ではデジカメのフレームレートが合わなくて、スクロールが多少カクカクしているところがありますが、実際はもっとスムースにスクロールしています。
Yahoo RSS ニュース電光掲示板として、手作りでここまでできれば十分でしょう。

比較のために、過去に作った有機EL ( OLED )のWEB連動電光掲示板を集めた動画はこんな感じになります。

やっぱり、一人で見る分には小さい文字でも良いですけど、他人にメッセージを伝えるには大きい文字でないとダメですね。
比較するとストロベリーリナックスさんのグラフィック有機EL は格段に文字が大きいことが分かると思います。

ちなみに、RSSニュースはもっと沢山のバリエーションがあります。
スポーツ報知や日経、ITmediaなど・・・、
パソコンで見る場合には以下のリンクを見てみてください。

Yahoo! ニュース RSS

ただ、残念なのが、このESP-WROOM-02 ( ESP8266 ) のSRAM メモリでは足りなくて、トピックス以外のニュースは見ることが出来ません
文字列の処理にメモリを食い過ぎてます。
これはいつか改善したいと思いますが、ここのところ忙しくてあまり詰めていません。
良い方法があったら教えていただけると幸いです。

ESP-WROOM-02 ( ESP8266 ) というものは、SRAMが膨大にあるかと思いきや、無作為に使ってしまうと意外と少ない容量です。
今回の場合、半角で1024文字を超えると例外エラーとなったり、文字列を受信できなかったりと、いろいろ悩みました。
スポーツ報知などの記事を取得すると、文字列が1500を超えて、電光掲示板に表示すらできませんでした。

メモリの節約をして、SPIFFSにWebSocketページを全て入れてしまうという手もあったのですが、使い勝手の点から今回は見送りました。
皆さんも試していただければ、もしかしたらスポーツ報知の記事も取得できるかもしれません。

では、前置きが長くなりましたが、解説していこうと思います。

1.使うもの

前回の記事と同様ですが、はじめて見る方のためにザッと紹介しておきます。

8ビット SPI I/Oエキスパンダ MCP23S08-E/P
Character_spi_01
秋月電子通商さんで販売。
Microchip製です。
これは5V駆動ですのでご注意ください
SPI通信の最大速度は10MHz です。
ESPr Developer ( ESP-WROOM-02, ESP8266 )ではGPIOピンが少ないので、これを使って増設します。
これで8bitモードパラレルのディスプレイが制御できます。

グラフィック有機ELモジュール100x16
Character_spi_08
ストロベリーリナックス ( Strawberry Linux )さんで販売しています。
コントローラーは WS0010 を使ってます。
キャラクタモードとグラフィックモードを切り替えできます。

ESPr Developer ( ESP-WROOM-02 開発ボード )
スイッチサイエンス製
Amazon.co.jp

これについては何度も当ブログで紹介しておりますが、日本の電波法をクリアした技適認証済み Wi-Fi モジュールです。ESP-WROOM-02 が安定して動作して、とても使いやすいです。
Arduino IDE で開発ができて、とてもお勧めです。
これの使い方については以下のページを参照してください。
https://www.mgo-tec.com/blog-entry-ss-wroom-howto01.html

0.1uF セラミックコンデンサー
I/Oエクスパンダの MCP23S08 チップの電源ノイズ対策のパスコンです。
積層セラミックで良いです。

サンハヤト製ニューブレッドボード 【SAD-101】
このブレッドボードがお勧めです。

その他、ジャンパーワイヤー、パソコン、USBケーブル等

2.接続、Arduino IDE 設定、SPIFFS設定、ライブラリインストール等

前回の記事 1~5 までの設定をを予め済ませておいてください。
SPIFFSアップロードのところでは、spiffs_01.txt のIPアドレスをご自分のルーターで割り当てたESPr Developer のローカルIPアドレスに書き換えることを忘れないでください

その他、今回はTimeライブラリを使用します。
時計を表示するために必要なのです。
Timeライブラリのインストール方法は以下の記事を参照してください。

Web News記事自動取得 OLED ( 有機EL )ミニ電光掲示板に16×16フリー日本語フォント( 東雲 ) を使う

その他、以下のライブラリは少々変更してバージョンアップしました。
yield()関数を1か所ずつ追加しただけですので、古いライブラリでも使用できると思います。

●ShinonomeFONTread ライブラリ

Beta ver 1.31 → Beta ver 1.33

●UTF8 to Shift_JIS ライブラリ

Beta ver 1.3 → Beta ver 1.33

ライブラリをバージョンアップしてインストールする場合は、必ず古いライブラリのフォルダごと削除してから再インストールすることを忘れないでください

3.スケッチの入力

今回は、WS0010 や MCP23S08 をライブラリ化していないので、かなり長いソースコードになります。

以下のコードをArduino IDE に入力してください。

/* 
 * ESPr Developer or ESP-WROOM-02 or ESP8266
 * GPIO Expander = MCP23S08 ( Microchip )
 * OLED = WS0010 ( Strawberry Linux )
 * The MIT License (MIT)
 * Copyright (c) 2016 mgo-tec 
 * License reference URL --> https://opensource.org/licenses/mit-license.php
 */

#include <SPI.h>
#include <ShinonomeFONTread.h> // beta ver1.33
#include <UTF8toSJIS.h> // beta ver1.33
#include <EasyWebSocket.h> //EasyWebSocket_SPIFFS beta ver1.45
#include <Hash.h>
#include <TimeLib.h> //timeライブラリver1.4の場合
#include <WiFiUdp.h>
 
//ESP8266 GPIO Register Direct Access 設定
#define PIN_OUT_SET *(volatile uint32_t *)0x60000304 //※PIN_OUTだけの場合は他のピンまで影響を与えるが、PIN_OUT_SETはターゲットピンのみアサインできる
#define PIN_ENABLE_SET *(volatile uint32_t *)0x60000310 //※PIN_ENABLEだけの場合は他のピンまで影響を与えるが、PIN_ENABLE_SETはターゲットピンのみアサインできる
#define PIN(a)  *(volatile uint32_t *)(0x60000328 + (a)*4) //#define設定で引数を使う方法

const char* ssid = "xxxx";
const char* password = "xxxx";

//GPIOエクスパンダ MCP23S08 のSPIインターフェースピン設定
const uint8_t sclk = 14;
const uint8_t mosi =13; //Master Output Slave Input ESP8266=Master,MCP23S08=slave 
const uint8_t MCP_CS = 0;
const uint8_t MCP_RST_pin =  16;
//OLED ピン設定 (8bitモード用)
const uint8_t OLED_RS_pin = 2;
const uint8_t OLED_RW_pin =  5;
const uint8_t OLED_E_pin = 4;

const char* UTF8SJIS_file = "/Utf8Sjis.tbl"; //UTF8 Shift_JIS 変換テーブルファイル名を記載しておく
const char* ZenkakuFontFile = "/shnmk16.bdf"; //全角フォントファイル名を定義
const char* HalfFontFile = "/shnm8x16r.bdf"; //半角フォントファイル名を定義

UTF8toSJIS u8ts;
ShinonomeFONTread SFR;
EasyWebSocket ews;

enum { MaxTxt = 800 , DispChar = 12}; //MaxTxt = スクロールする文字列の最大数
                                      //DispChar = ディスプレイに表示できる半角文字の数
uint8_t font_buf[MaxTxt][16];
uint8_t SnnmDotOut[DispChar][16];
uint8_t Next_buf[DispChar][16];
uint8_t dummy_font_buf[16];

String html_str1 = "", html_str2 = "", html_str3 = "", html_str4 = "", html_str5 = "", html_str6 = "", html_str7 = "";

int PingSendTime = 30000;

//-------NTPサーバー定義-----------------------------
IPAddress timeServer(132, 163, 4, 101); // time-a.timefreq.bldrdoc.gov; //(129, 6, 15, 28)time.nist.gov NTP server
const int NTP_PACKET_SIZE = 48; // NTP time stamp is in the first 48 bytes of the message
byte packetBuffer[ NTP_PACKET_SIZE]; //buffer to hold incoming and outgoing packets
const int timeZone = 9;     // Tokyo
WiFiUDP Udp;
unsigned int localPort = 8888;  // local port to listen for UDP packets
time_t prevDisplay = 0; // when the digital clock was displayed
uint32_t LastNTP_Get = 0;
//-----時計表示引数定義------------------------------ 
boolean clock_Disp_Set = false, clock_Disp_OUT = false;
uint8_t clock_Select = 0;
uint32_t sec_colon_time = 0;
//-----Yahoo記事取得引数定義-------------------------
uint32_t Web_time = 0;
boolean Web_first_get = true, Text_Box_On = false;
uint8_t Yahoo_get = 1;
uint8_t sj_txt[MaxTxt];
uint16_t sj_cnt = 0;
uint16_t sj_length;
uint32_t SCLtime;
uint16_t Scrolle_speed = 8;
uint8_t scl_cnt = 0;

//***************セットアップ**********************************************
void setup() {
  char c1[64] = "----- W a i t -----."; //1列最大64文字格納。そのうち20文字表示
  char c2[17] = "Connecting Wi-Fi"; //1列最大64文字格納。そのうち20文字表示
  int i, j;
  Serial.begin(115200);
  Serial.println();
  delay(10);
  //ESP8266 GPIO Register Direct Access 設定。ターゲットピンのみアサイン。
  PIN_OUT_SET = (1<<OLED_RW_pin | 1<<OLED_E_pin | 1<<OLED_RS_pin | 1<<MCP_CS);
  PIN_ENABLE_SET = (1<<OLED_RW_pin | 1<<OLED_E_pin | 1<<OLED_RS_pin | 1<<MCP_CS);
  PIN(MCP_CS) = 0; //HIGH
  PIN(OLED_E_pin) = 1; //LOW
  pinMode(MCP_RST_pin, OUTPUT);
  digitalWrite(MCP_RST_pin, HIGH);

  MCP23S08_Ini(); //GPIOエキスパンダ MCP23S08初期化
  delay(1000);
  OLED_Character_Ini(); //OLEDキャラクタモード初期化
  for(i=0; i<64; i++){ //1列最大64文字格納。そのうち20文字表示
    OLED_DataWrite(c1[i]);
  }
  for(i=0; i<16; i++){ //1列最大64文字格納。そのうち20文字表示
    OLED_DataWrite(c2[i]);
  }

  html_str1 += "<body style='background:#000; color:#fff;'>\r\n";
  html_str1 += "<font size=3>\r\n";
  html_str1 += "ESP-WROOM-02(ESP8266)\r\n";
  html_str1 += "<br>\r\n";
  html_str1 += "EasyWebSocket Beta1.45 Sample\r\n";
  html_str1 += "</font><br><br>\r\n";
  html_str1 += ews.EWS_TextBox_Send("txt1", "Hello Easy WebSocket Beta1.3","送信");
  html_str2 += "<br>Scrolle Speed\r\n";
  html_str2 += ews.EWS_Canvas_Slider_T("SPEED",200,40,"#777777","#0000ff");
  html_str2 += "<br><br><br>\r\n";
  html_str3 += ews.EWS_On_Momentary_Button("Clock", "時計(秒表示)", 100,25,15,"#ffffff","#00aa00");
  html_str3 += "  \r\n";
  html_str3 += ews.EWS_On_Momentary_Button("clock", "時計(月日表示)", 120,25,15,"#ffffff","#00aa00");
  html_str3 += "<br><br>\r\n";
  html_str4 += "<fieldset style='border-style:solid; border-color:gray'>\r\n";
  html_str4 += "<legend style='font-style: italic; color:grey;'>Yahoo! News Topics</legend>\r\n";
  html_str4 += ews.EWS_On_Momentary_Button("1Yahoo", "Yahoo!ニュースTOP", 200,30,15,"white","#ff7766");
  html_str4 += "<br><br>\r\n";
  html_str4 += ews.EWS_On_Momentary_Button("2Yahoo", "Yahoo!エンタメ", 200,30,15,"white","#ff7766");
  html_str4 += "<br><br>\r\n";
  html_str4 += ews.EWS_On_Momentary_Button("3Yahoo", "Yahoo!スポーツ", 200,30,15,"white","#ff7766");
  html_str4 += "<br><br>\r\n";
  html_str4 += ews.EWS_On_Momentary_Button("4Yahoo", "Yahoo!天気予報(東京)", 200,30,15,"white","#ff7766");
  html_str4 += "</fieldset>\r\n";
  html_str4 += "<br>\r\n";
  html_str5 += ews.EWS_Status_Text2("WebSocket Status","#555", 20,"#FF00FF");
  html_str5 += "<br>\r\n";
  html_str7 += ews.EWS_WebSocket_Reconnection_Button2("WS-Reconnect", "grey", 200, 40, "black" , 17);
  html_str7 += "<br><br>\r\n";  
  html_str7 += ews.EWS_Close_Button2("WS CLOSE", "#ccc", 150, 40, "red", 17);
  html_str7 += ews.EWS_Window_ReLoad_Button2("ReLoad", "#ccc", 150, 40, "blue", 17);
  html_str7 += "</body></html>\r\n";

  for(i=0; i<16; i++) { //初期化しておく
    for(j=0; j<16; j++){
      font_buf[i][j] = 0; SnnmDotOut[i][j] = 0; Next_buf[i][j] = 0;
    }
  }

  ews.AP_Connect(ssid, password);
  delay(1000);

  //NTPサーバーから時刻を取得
  Udp.begin(localPort);
  setSyncProvider(getNtpTime);
  delay(1000);
  OLED_Graphic_Ini(); //OLED WS0010 初期化
  
  SCLtime = millis();
  prevDisplay = now();
  LastNTP_Get = millis();
}

void loop() {
  ews.EWS_HandShake(html_str1, html_str2, html_str3, html_str4, html_str5, html_str6, html_str7);
  
  int i,j;
  String ret_str = "";

  if(ret_str != "_close"){
    ret_str = ews.EWS_ESP8266CharReceive(PingSendTime);
    if(ret_str != "\0"){
      if(ret_str != "Ping"){
        if(ret_str[0] != 't'){
          int ws_data = (ret_str[0]-0x30)*100 + (ret_str[1]-0x30)*10 + (ret_str[2]-0x30);
          switch(ret_str[4]){
            case 'S':
              Scrolle_speed = floor((200-ws_data)/(200/40)); //スマホスライダー値(0-200)で、値を(40-0)に変更したい場合
              Serial.print("Scrolle_speed = "); Serial.println(Scrolle_speed);
              break;
            case 'C':
              clock_Disp_Set = true; Text_Box_On = false;
              clock_Select = 0;
              break;
            case 'c':
              clock_Disp_Set = true; Text_Box_On = false;
              clock_Select = 1;
              sec_colon_time = millis();
              break;
            case '1':
              Yahoo_get = 1;
              Web_first_get = true; clock_Disp_Set = false; Text_Box_On = false;
              break;
            case '2':
              Yahoo_get = 2;
              Web_first_get = true; clock_Disp_Set = false; Text_Box_On = false;
              break;
            case '3':
              Yahoo_get = 3;
              Web_first_get = true; clock_Disp_Set = false; Text_Box_On = false;
              break;
            case '4':
              Yahoo_get = 4;
              Web_first_get = true; clock_Disp_Set = false; Text_Box_On = false;
              break;
          }
          Serial.println(ret_str);
        }else if(ret_str[0] == 't'){
          String txt;
          txt = ret_str.substring(ret_str.indexOf('|')+1, ret_str.length()-1);
          sj_cnt = 0;
          int t_len = txt.length();
          if(t_len < 20){
            for(i=0; i<(20-t_len); i++){
              txt += ' ';
            }
          }
          txt = "  " + txt;
          u8ts.UTF8_to_SJIS_str_cnv(UTF8SJIS_file, txt, sj_txt, &sj_length);
          SFR.SjisToShinonome16FontRead_ALL(ZenkakuFontFile, HalfFontFile, 0, 0, sj_txt, sj_length, font_buf);
          for(i=0; i<sj_length; i++){
            Serial.write(sj_txt[i]);
          }
          Serial.println();
          for(i=0; i<16; i++) dummy_font_buf[i] = font_buf[0][i];
          Text_Box_On = true; clock_Disp_Set = false;
        }
      }
    }
  }else if(ret_str == "_close"){
    ret_str = "";
  }
  
  if((clock_Disp_Set == false) && (millis()-SCLtime > Scrolle_speed)){ //文字スクロール
    Scroller_8x16x12_Dot_Replace(DispChar, Next_buf, SnnmDotOut, dummy_font_buf, font_buf, sj_length, &scl_cnt, &sj_cnt);
    OLED_8x16x12_Font_DisplayOut(DispChar, SnnmDotOut);
    SCLtime = millis();
  }

  if(clock_Disp_Set == true){ //時計表示
    Clock_Display(clock_Select);
  }

  if(millis()-LastNTP_Get > 3600000UL){ //1時間ごとに時刻補正
    setSyncProvider(getNtpTime);
    LastNTP_Get = millis();
    Serial.println("---------------NTP time acquisition completed");
  }

  if(Text_Box_On == false && clock_Disp_Set == false){
    if(Web_first_get == true || millis()-Web_time > 600000UL){ //Web記事を10分毎に取得
      String news_str;
      String news1_1_target_ip;

      char Web_h[3], Web_m[3];
      sprintf(Web_h, "%02d", hour());//ゼロを空白で埋める場合は%2dとする
      sprintf(Web_m, "%02d", minute());
      news_str = String("◆ ") + String(Web_h) + ":" + String(Web_m) + " ";

      switch(Yahoo_get){
        case 1:          
          news1_1_target_ip = "/pickup/rss.xml"; // Yahoo!ニューストピックストップ
          news_str += ews.EWS_Web_Get("news.yahoo.co.jp", news1_1_target_ip, '\n', "</rss>", "<title>", "</title>", "◆ ");
          break;
        case 2:
          news1_1_target_ip = "/pickup/entertainment/rss.xml"; // Yahoo!エンタメ
          news_str += ews.EWS_Web_Get("news.yahoo.co.jp", news1_1_target_ip, '\n', "</rss>", "<title>", "</title>", "◆ ");
          break;
        case 3:
          news1_1_target_ip = "/pickup/sports/rss.xml"; // Yahoo!スポーツ
          news_str += ews.EWS_Web_Get("news.yahoo.co.jp", news1_1_target_ip, '\n', "</rss>", "<title>", "</title>", "◆ ");
          break;
        case 4:
          news1_1_target_ip = "/rss/days/4410.xml"; // Yahoo!天気予報(東京)
          news_str += "東京の天気◆";
          news_str += ews.EWS_Web_Get("rss.weather.yahoo.co.jp", news1_1_target_ip, '>', "</rss>", "【", "Yahoo!", "【");
          news_str.replace(" 東京(東京) ","");
          break;
      }

      Serial.print("\r\nWebGet str = ");
      news_str.replace("&amp;","&"); //XMLソースの場合、&が正しく表示されないので、全角に置き換える
      u8ts.UTF8_to_SJIS_str_cnv(UTF8SJIS_file, news_str, sj_txt, &sj_length);
      SFR.SjisToShinonome16FontRead_ALL(ZenkakuFontFile, HalfFontFile, 0, 0, sj_txt, sj_length, font_buf);
      for(i=0; i<16; i++) dummy_font_buf[i] = font_buf[0][i]; //1文字目を予め入力しておく
      
      for(uint16_t iy=0; iy<sj_length; iy++){
        Serial.write(sj_txt[iy]);
        yield();
      }
      Serial.println(); Serial.print("sj_length="); Serial.println(sj_length);
      news_str ="";
      scl_cnt = 0; sj_cnt = 0;
      Web_first_get = false;
      Web_time = millis();
    }
  }
}
//*********GPIOエキスパンダMCP23S08初期化関数********************
void MCP23S08_Ini(){ 
  SPI.begin();
  SPI.setFrequency(10000000); //MCP23S08 Max 10MHz
  SPI.setDataMode(SPI_MODE0);

  digitalWrite(MCP_RST_pin, HIGH);
  delay(10);
  digitalWrite(MCP_RST_pin, LOW);
  delay(100);
  digitalWrite(MCP_RST_pin, HIGH);
  delay(50);
  
  PIN(MCP_CS) = 1; //LOW
    delay(1);
    SPI.write(B01000000);//デバイスオペコードB01000000 & チップaddress 000
    SPI.write(0x06);//MCP設定レジスタIOCON 
    SPI.write(B00100000);//シーケンシャル動作禁止、SDAスルーレート許可、ハードウェアアドレスピン禁止、INT出力LOW、アクティブドライブ
    delay(1);
  PIN(MCP_CS) = 0; //HIGH
  
  PIN(MCP_CS) = 1; //LOW
    delay(1);
    SPI.write(B01000000);//デバイスオペコードB01000000 & チップaddress 000
    SPI.write(0x00);// I/O方向レジスタ(IODIR)設定
    SPI.write(B00000000);//8pin ALL OUTPUT
    delay(1);
  PIN(MCP_CS) = 0; //HIGH
}
//*********有機ELキャラクタモード初期化関数********************
void OLED_Character_Ini(){ //OLED WS0100 初期化  
  PIN(OLED_RW_pin) = 1; //LOW
  PIN(OLED_E_pin) = 1; //LOW
  PIN(OLED_RS_pin) = 1; //LOW

  //OLEDリセット
  delayMicroseconds(10);
  OLED_CommandWrite(B00000001);//Display Clear
  delay(10);
  OLED_CommandWrite(B00010011);//Character MODE ON, Power is turned OFF
  delay(10); //リセットは一旦電源を切るべし
  OLED_CommandWrite(B00001000);//Display OFF, Cursor OFF, Brinking OFF 
  delay(1000);
  //ここからディスプレイ初期化設定
  OLED_CommandWrite(B00010111);//Character MODE ON, Power is turned ON
  delay(10);
  OLED_CommandWrite(B00111000);//Function Set 8bit-mode 2Line-display
  delay(10);
  OLED_CommandWrite(B00001111);//Display On, Cursor ON, Brinking on 
  delay(10);
  OLED_CommandWrite(B00000001);//Display Clear
  delay(10);
  OLED_CommandWrite(B00000010);//Return HOME
  delay(10);
  OLED_CommandWrite(B00000110);//Entry Mode Set, Increment, No-Shift
  delay(10);
}
//*********有機ELグラフィックスモード初期化関数********************
void OLED_Graphic_Ini(){
  PIN(OLED_E_pin) = 1; //LOW
  PIN(OLED_RW_pin) = 1; //LOW
  PIN(OLED_RS_pin) = 1; //LOW

  //OLEDリセット
  delayMicroseconds(10);
  OLED_CommandWrite(B00000001);//Display Clear
  delay(10);
  OLED_CommandWrite(B00010011);//Power is turned OFF, Character MODE ON
  delay(10); //リセットは一旦電源を切るべし
  OLED_CommandWrite(B00001000);//Display OFF, Cursor OFF, Brinking OFF 
  delay(1000);
  //ここからOLED初期化設定
  OLED_CommandWrite(B00011111);//Graphic MODE ON, Power is turned ON
  delay(20);
  OLED_CommandWrite(B00111000);//Function Set 8bit-mode 2Line-display
  delay(10);
  OLED_CommandWrite(B00001100);//Display On, Cursor OFF, Brinking OFF
  delay(10);
  OLED_CommandWrite(B00000001);//Display Clear
  delay(10);
  OLED_CommandWrite(B00000110);//Entry Mode Set インクリメント&シフトなし
  delay(10);
}
//*********電光掲示板風スクロール関数 8x16ドット 12文字用********************
void Scroller_8x16x12_Dot_Replace(uint8_t disp_char, uint8_t next_buff1[][16], uint8_t scl_buff1[][16], uint8_t* Orign_buff1, uint8_t font_buf1[][16], uint16_t Length, uint8_t* scl_cnt1, uint16_t* sj_cnt1){
  int8_t i, j;
  
  if(*scl_cnt1 == 8){
    *scl_cnt1 = 0;
    (*sj_cnt1)++;
    if(*sj_cnt1 >= Length){
      *sj_cnt1 = 0;
    }
    for(i=0; i<16; i++) Orign_buff1[i] = font_buf1[*sj_cnt1][i];
  }

  for(i=15; i>=0; i--){ //まず、一番左側文字をビットシフト
    next_buff1[disp_char-1][i] = ( next_buff1[disp_char-1][i] | ( scl_buff1[disp_char-1][i] & B10000000 ));
    scl_buff1[disp_char-1][i] = scl_buff1[disp_char-1][i]<<1;
    scl_buff1[disp_char-1][i] = ( scl_buff1[disp_char-1][i] | (( Orign_buff1[i] & B10000000 )>>7));
    Orign_buff1[i] = Orign_buff1[i]<<1;
  }
  for(i=disp_char-2; i>=0; i--){ //次にその他文字をビットシフト
    for(j=15; j>=0; j--){
      next_buff1[i][j] = ( next_buff1[i][j] | ( scl_buff1[i][j] & B10000000 ));
      scl_buff1[i][j] = scl_buff1[i][j]<<1;
      scl_buff1[i][j] = ( scl_buff1[i][j] | (( next_buff1[i+1][j] & B10000000 )>>7));
      next_buff1[i+1][j] = next_buff1[i+1][j]<<1;
    }
  }
  (*scl_cnt1)++;
}
//*********有機EL 漢字フォント表示出力関数********************
void OLED_8x16x12_Font_DisplayOut(uint8_t len, uint8_t buf[][16]){
  uint8_t i, j, k;
  uint8_t Rbuf[len][16];
    
  for(k=0; k<len; k++){ //+90 Dot Rotation
    for(j=0; j<8; j++){
      for(i=0; i<8; i++){
        bitWrite(Rbuf[k][j], i,bitRead(buf[k][i], 7-j));
      }
    }
    for(j=8; j<16; j++){
      for(i=8; i<16; i++){
        bitWrite(Rbuf[k][j], i-8,bitRead(buf[k][i], 15-j));
      }
    }
  }
 
  for(j=0; j<len; j++){ //OLED Display OUT
    OLED_XYset(j*8, 0);
    for(i=0; i<8; i++){
      OLED_DataWrite(Rbuf[j][i]);
    }
    OLED_XYset(j*8, 1);
    for(i=8; i<16; i++){
      OLED_DataWrite(Rbuf[j][i]);
    }
  }
}
//*********有機EL XY設定関数******************************
void OLED_XYset(uint8_t x, uint8_t y){
   OLED_CommandWrite(0x80 + x);  
   OLED_CommandWrite(0x40 + y);
}
//*********有機EL 設定コマンド送信関数********************
void OLED_CommandWrite(uint8_t b){
  PIN(OLED_RS_pin) = 1; //1=LOW
  MCP23S08_SpiCommandWrite(b);
  PIN(OLED_E_pin) = 0;//EピンをHIGH→LOWへ変化させると書き込み完了
  PIN(OLED_E_pin) = 1;
}
//*********有機EL データ送信関数**************************
void OLED_DataWrite(uint8_t b){
  PIN(OLED_RS_pin) = 0; //0=HIGH
  MCP23S08_SpiCommandWrite(b);
  PIN(OLED_E_pin) = 0;//EピンをHIGH→LOWへ変化させると書き込み完了
  PIN(OLED_E_pin) = 1;
}
//*********GPIOエキスパンダMCP23S08コマンド設定関数*******
void MCP23S08_SpiCommandWrite(uint8_t b){
  PIN(MCP_CS) = 1; //1=LOW
    SPI.write(B01000000);
    SPI.write(0x0A);
    SPI.write(b);
  PIN(MCP_CS) = 0; //0=HIGH
}
//***************時計表示関数*****************************
void Clock_Display(uint8_t sec_or_ymd){
  String time_str;
  uint8_t time_sj_txt[25];
  uint16_t time_sj_length;
  char month_chr[3], day_chr[3], h_chr[3], m_chr[3], s_chr[3];
  const char* week_jp[7] = {"日","月","火","水","木","金","土"};
  uint8_t time_dot[12][16];
  
  sprintf(h_chr, "%2d", hour());//ゼロを空白で埋める場合は%2dとすれば良い
  sprintf(m_chr, "%02d", minute());
  sprintf(s_chr, "%02d", second());
  sprintf(month_chr, "%2d", month());
  sprintf(day_chr, "%02d", day());
  
  if(now() != prevDisplay){ //ここから時刻表示設定
    switch(sec_or_ymd){
      case 0:
        time_str = String(h_chr) + ':' + String(m_chr) + ':' + String(s_chr) + '(' + String(week_jp[weekday()-1]) + ')';
        break;
      case 1:
        time_str = String(month_chr) + '/' + String(day_chr) + String(week_jp[weekday()-1]) + String(h_chr) + ':' + String(m_chr);
        sec_colon_time = millis();
        break;
    }
    clock_Disp_OUT = true;
    prevDisplay = now();
  }else if((millis()-sec_colon_time >= 500) && (sec_or_ymd == 1) && (clock_Disp_OUT == false)){
    time_str = String(month_chr) + '/' + String(day_chr) + String(week_jp[weekday()-1]) + String(h_chr) + ' ' + String(m_chr);
    clock_Disp_OUT = true;
  }
  if(clock_Disp_OUT == true){
    u8ts.UTF8_to_SJIS_str_cnv(UTF8SJIS_file, time_str, time_sj_txt, &time_sj_length);
    SFR.SjisToShinonome16FontRead_ALL(ZenkakuFontFile, HalfFontFile, 0, 0, time_sj_txt, time_sj_length, time_dot);
    OLED_8x16x12_Font_DisplayOut(DispChar, time_dot);
    clock_Disp_OUT = false;
  }
}
//*************************NTP Time**************************************
time_t getNtpTime(){
  while (Udp.parsePacket() > 0) ; // discard any previously received packets
  Serial.println("Transmit NTP Request");
  sendNTPpacket(timeServer);
  uint32_t beginWait = millis();
  while (millis() - beginWait < 1500) {
    int size = Udp.parsePacket();
    if (size >= NTP_PACKET_SIZE) {
      Serial.println("Receive NTP Response");
      Udp.read(packetBuffer, NTP_PACKET_SIZE);  // read packet into the buffer
      unsigned long secsSince1900;
      // convert four bytes starting at location 40 to a long integer
      secsSince1900 =  (unsigned long)packetBuffer[40] << 24;
      secsSince1900 |= (unsigned long)packetBuffer[41] << 16;
      secsSince1900 |= (unsigned long)packetBuffer[42] << 8;
      secsSince1900 |= (unsigned long)packetBuffer[43];
      return secsSince1900 - 2208988800UL + timeZone * SECS_PER_HOUR;
    }
  }
  Serial.println("No NTP Response :-(");
  return 0; // return 0 if unable to get the time
}
//*************************NTP Time**************************************
void sendNTPpacket(IPAddress &address){
  memset(packetBuffer, 0, NTP_PACKET_SIZE);

  packetBuffer[0] = 0b11100011;   // LI, Version, Mode
  packetBuffer[1] = 0;     // Stratum, or type of clock
  packetBuffer[2] = 6;     // Polling Interval
  packetBuffer[3] = 0xEC;  // Peer Clock Precision
  packetBuffer[12]  = 49;
  packetBuffer[13]  = 0x4E;
  packetBuffer[14]  = 49;
  packetBuffer[15]  = 52;
            
  Udp.beginPacket(address, 123); //NTP requests are to port 123
  Udp.write(packetBuffer, NTP_PACKET_SIZE);
  Udp.endPacket();
}

(Web上でソースコードを配布する場合はライセンスを記述しなければ、自由に配布できないということですので、MITライセンスとしておきます。)

【解説】

前回の記事のスケッチと重複するところは省いて説明します。

●15行:
今回で使用する Arduino 標準の Timeライブラリ です。
時計表示に使用します。

●16行:
インターネット上のNTPサーバーから標準時刻を取得するためのWi-Fi UDP通信ライブラリです。
これはArduino core for Wi-Fiチップライブラリ標準のものです。

●23-24行:
xxxxのところをご自分のルーターの環境に合わせて書き換えてください。

●44行:
電光掲示板に流す文字列の最大数を半角で800 としています。
1024 を超えると動作が不安定になります。
先にも述べたように、このプログラムではSRAMを多量に消費していますので、安定動作のためには800くらいが良いと思います。
因みに、Yahoo! RSS ニュースのトピックスでは多くて600文字弱なので、これで十分です。
しかし、トピックス以外の記事は半角で1500を超えるので、例外エラーとなり、動作しませんのでご注意ください

●56-63行:
インターネット上のNTPサーバーから時刻を自動取得して補正するための定義です。
これは、Arduino core for ESP8266 ライブラリの Arduino IDEサンプルスケッチにあるものをそのまま流用してます。

●105-136行:
スマホのブラウザに表示するHTMLタグのうち、body要素内のものをString文字列で定義してます。
ヘッダ部分はSPIFFSでアップロードした spiffs_01.txt に記述してあります。
これは従来の自作EasyWebSocketライブラリ関数と同じ使い方です。
ちょっとしたポイントとして、119-120行と128行で使用している fieldsetタグが使い勝手が良いのでおすすめです。
要するに複数のボタンを囲って表示させるときに便利です。

●148-150行:
ここでNTPサーバーから時刻を取得して、ESPr Developer ( ESP-WROOM-02, ESP8266 ) の内部時計を補正してます。
500-538行でも関数化していますので参照してみてください。
ちなみにこの辺はArduino IDE サンプルスケッチをそのまま流用しています。
150行目のdelay(1000); はNTP時計受信後の動作を安定させるために、少々時間が必要なためのものです。

●175-179行:
115-117行で設定した、スマホボタンのID文字列の頭文字を受信したら、時計表示を切り替えるフラグを立てます。
“Clock”  の ‘ C ‘ を受信したら clock_Select = 0 となり、時計の秒表示セットとなります。そして、OLED に表示されます。
“clock” の小文字 ‘ c ‘ を受信したら月日曜日表示となるフラグを立てます。

●184-199行:
ここでスマホ上のYahoo記事取得ボタンのうち、どのボタンが押されたかを検知して、フラグを立てます。
検知したら時計表示やテキストボックス送信フラグをfalse にするところがポイントです。

●202-221行:
スマホ上のテキストボックスから送信された文字列をOLEDに表示するものです。
前回の記事と同様です。

●228-232行:
ここを通過する度に、OLED ディスプレイの文字を1ドットずつ左へずらしていきます。
229行でSRAMメモリ上でドットをずらし、230行でOLEDに一気に表示させて塗り替えています。

●234-236行:
ここで、スマホの時計表示ボタンが押されたら OLED ディスプレイに時計表示するようにしてます。
235行の関数は、462-498行を見てください。

●238-242行:
ここで1時間(3600秒)毎にNTPサーバーから時刻を取得して補正してます。

●244-291行:
245行で10分毎に通過する設定をしています。
注意していただきたいのは、この時間が短すぎると相手サーバーに迷惑をかけてしまうので、十分長く時間を取ってください
ここでは、スマホのYahoo!記事取得ボタンが押されたら、ここで記事取得するURLを分類してます。
先にも述べましたが、Yahoo! RSS 記事はトピックストップ記事のみ有効です。
他の記事は文字数が多すぎて例外エラーが出てしまうので、ご注意ください。
257, 261, 265, 270行が、自作のEasyWebSocketライブラリで関数化したものを使ってます。
Yahoo! RSS サイトの記事のソースコードを見ると、title で区切られたところが欲しいので、それを抽出してます。
この関数の使い方は以下の記事で紹介してますので、参照してみてください。

自作LED電光掲示板に Yahooニュースや 天気予報 を表示させ、さらに NTP 時計機能追加しました

ちなみに、天気予報では不要な文字列が多いので、271行のStringクラスの replace を使って消去してます。

●462-498行:
ここは、OLEDディスプレイに時計表示するための関数です。
月日表示のときには、コロン(:)を0.5秒毎に点滅させることが意外と面倒でした。
そこでちょっと複雑になっています。

●499-538行:
これはNTPサーバー時刻取得用のArduino IDE サンプルスケッチそのままの流用です。

以上、スケッチのザットした説明でした。
細かく解説すると労力と疲労が半端ないので、この辺にしておきます。

4.コンパイル書き込み、実行

では、コンパイル実行してみてください。
先の動画のように表示されればOKです。

そして、スマホ側ではこんな感じに表示されますので、いろいろと操作してみてください。

WS0010_yahoo02

ちなみに、Yahoo! RSS 記事取得ボタンを押したら、WebSocket通信は強制切断されます。
つまり、記事取得はHTTP通信で、WebSocket通信のようなリアルタイム双方向プッシュ通信とは異なるので、そういう方式にしてます。

5.まとめ

以上で、ようやく理想の大きさのニュース記事取得電光掲示板ができました。

正直言って、これはいろいろと実用レベルで使えますよ!!!
いままで懸案事項だったタイマー機能を盛り込めば、介護などで薬の飲み忘れ防止掲示板なんかも作れそうです。
近々これ用のケースを作ってみたいと思ってます。

ただ、問題は、SRAMが一杯いっぱいだというところです。
SRAMの増設も検討するとか、グローバル変数領域のメモリをうまく節約するしかありません。

ということで、今回はここまでです。
ではまた・・・。

スポンサーリンク

mgo-tec電子工作 関連コンテンツ ( 広告含む )
Amazon.co.jp 当ブログのおすすめ






投稿者:

mgo-tec

Arduino , ESP32 ( ESP-WROOM-32 ) , ESP8266 ( ESP-WROOM-02 )等を使って、主にスマホと連携した電子工作やプログラミング記事を書いてます。ライブラリも作ったりしてます。趣味、独学でやってますので、動作保証はしません。
電子回路やプログラミングの専門家ではありません。
畑違いの仕事をしてます。
でも、少しだけ電気の知識が必要な仕事なので、電気工事士や工事担任者等の資格は持ってます。

コメントを残す

メールアドレスが公開されることはありません。

*画像の文字を入力してください。(スパム防止の為)