VPython Device fF自由落體實驗(Edutalk3) 假裝成 Ball-trow1    
  VPython DA from Edutalk       網頁上挑選顏色複製#十六進位代碼       Color picker    
* 請注意目前 IoTtalk 伺服器 上的 Free-Fall2 只接受一個參數
          ==> 都還沒用過 IoTtalk 物聯網平台請看這 12分鐘影片
** EduTalk 總部在交大       **台大石明豐教授 Vpython+Physics 課程文件(pdf,5.28MB)   
** 改好的 Ball-Free-fall 可以放 IoTtalk假裝 Ball-throw1(也試改中和高中王一哲老師程式)
** 臺灣大學物理系石明豐教授的 Vphysics 網站
** 點這到 GlowScript 官方網站     GlowScript Examples
** 點這到 VPython 官方網站       這是 VPython-Jupyter 的 github repository.
👉 點這看 IoTtalk 物聯網平台(交大林一平教授團隊研發)入門手冊 iottalk.vip/000/  
👉 Python 還不太會 或要看 Flask 入門的可點這 到 https://iottalk.vip/6/
   (*看 w.py 範例   *看 w2.py 範例   *看 w3.py 範例-用到 template )
** 複習自己電腦上的 Bulb Device Application (Web version)
** 如何做點了才開啟VPDA網頁 -- Homepage(首頁) for ALL VPython Device Applications  
      (解決 Google 瀏覽器 『使用者沒跟網頁互動過則不給自動播放影音』 的規定 )
** Flask-Login Library (©MIT License) 入門  範例  Tutorial  Teaching  More...
** 精簡版 Flask + Python + IoTtalk DA  ** VPython 版的 Dummy_Device (ODF only)
** 爬蟲 爬爬爬 抓取 CWB 氣象局資料 做微氣象站 (as Dummy_Device) -- 有影片
Turtle Graphics 烏龜繪圖 弄成 IoTtalk DA 假裝 Ball-throw1  更多Turtle 繪圖範例
  * 「刻意練習」是一種專注、一致並且以目標為導向的訓練,它重質量而不是數量。

0523加分題的參考影片在這:  https://youtu.be/zDfoH9MnKg4
  除了最後一項沒詳細步驟, 其它前五項在影片中都有詳細Demo與解說!
至於(6)最後一項, 其實也很簡單, (05/27 更新, 已補充影片在這段下方)
就是從Edutalk抓回 .py 檔案後先改為符合 IoTtalk 的 VPython DA 規定!
其實 Edutalk 和 IoTtalk 只差在 profile 用 df_list 或是 odf_list;
所以, 改一個字(加一個小寫 o)存檔, 搭配正確的 dai.js, HTML 檔案;
然後到 IoTtalk 系統建立一個對應的 DM (Device Model) 就可以用了!
  至於如何建立一個 DM 之前已經教過喔, 
可參考0505P2_DMM_影片 https://youtu.be/HTR-QkqKUOM(Device Model Management)
當然如不自己建立新 DM (Device Model) 則須配合現存的 DM 用其 dm_name 和 odf_list,
可以挑選任一個其 ODF 個數不比我們少的 DM, 例如我範例是挑選 Ball-throw1 有三個 ODF;
  修改 .py 的 code 要確定 odf_list 內各 ODF 都有對應的函數(多看幾個範例就會了:-)
檢查 dai.js, 確認要連的 IoTtalk server 對不對(set_endpoint 那句, 應該在 LINE 5);
然後要把 dai.js 改為用到正確的 .py 檔(注意路徑), 大約在 LINE 132;
還有 HTML 檔案要用到正確的 dai.js 和 dan.js檔案(其它都用 IoTtalk Server 的即可),
最後, 在網站檔(w80.py)增加規則並利用 return redirect("你的網址") 轉到正確網頁!
如果這樣還是不太懂, 詳細步驟請往下看 ..
[05/27]也可看這補充從 Edutalk 把自由落體實驗 的 code 偷回弄成 VP_DA 在 127.0.0.1/b3 的影片
----------------------------------------------------------------------- * Previous 0516加分題的參考影片在這: https://youtu.be/_rjg5ggWD9g -----------------------------------------------------------------------
關於如何從 Edutalk 抓 自由落體實驗 回來改成 IoTtalk 的 VPython DA ?
**我改好的自由落體實驗程式碼(假裝成Ball-throw1)等這週三(05/27)晚上再公佈於這網頁後面:-)
  不過建議要, 要, 要 .. 先自己試著改, 十幾分鐘改不出來再參考我改好的, 
  還有, 參考過後還是要稍微研究與思考才會進步喔!!

(1)這裡假設你已經會用 Flask + Python 在自己電腦建立一個網站
   如果還不會, 請看 這次 05/23 加分題影片
   或是看上次 05/16 加分題影片     ** 在 iottalk.vip/6/ 有詳細解說
(2)以下假設網站的 .py 檔案是 w80.pyw80.py 可以在 https://iottalk.vip/b1 網頁內找到!
(3)原先規定檔案放 /static
   但如果 DA 越來越多會太多檔案, 所以這裡改為另放在一個子目錄 例如 b3
   這樣在 /static/b3/ 內會有三個檔案: html, dai.js, .py 檔
   之後如又有新的 VPython DA,
   只要把 b3 這子目錄複製一份成例如 b4 子目錄,
   並修改裡面的 .py 檔案內容即可(三個檔名以及 html 和 dai.js 內容都可以不改!)
   提醒要檢查 dai.js, 確認要連的 IoTtalk server 對不對(set_endpoint 那句, 應該在 LINE 5);
   也順便檢查(在 dai.js 的大約 LINE 132)是否會指去確的 .py 檔;
   然後就可以用  127.0.0.1/static/b4/index.html
*** Home page      
(4)為了解決沒互動過就不給播放聲音的問題: 可以建一個 VP List 網頁, 裡面放各個 VP DA 的連結讓 USER 點了可跳過去DA網頁, (對啦, 就是類似 IoTtalkVPython Device List 啦!) 這樣, 原先我們為了互動加入的按鈕就可以去掉, 恢復成 <body> 內只有兩列的寫法! 這 VP List 網頁很簡單, 如下:
<!DOCTYPE html>
<html>
<head>
 <title>My VP DA on my Flask Web site </title>
 </head>
<body style="margin-left:2vw;background:lightcyan;">
<b><font color=blue size=6><font color=red size=7>My VP DA List</font>
<img src="http://clipart-library.com/images/BTaropdpc.gif" width="128" align="left">
<pre><a target=_blank href="/111">Ball-throw1 丟球+三個參數</a>
 <a target=_blank href="/222">Ball-throw2 丟球+一個參數</a>
 <a target=_blank href="/b3/">自由落體 假裝 Ball-throw1 (三參數)</a
 ></font></pre>
 <h2 style="color:orange;">====== ====== ====== ====== ===== ======</h2>
 <div style="background:red;height:3px;width:58vw;"></div>
 <div style="background:green;height:3px;width:63vw;margin-top:2px;"></div>
 <div style="background:blue;height:3px;width:58vw;margin-top:2px;"></div>
</body>
</html>
*再說一次, 可把上面這 HTML 檔案當作首頁, 這樣只要打 127.0.0.1 就出現這頁,
 然後點了再跳過去的各 VP DA 網頁就不必有那個前置按鈕, 仍可以播放聲音!
 因為點了跳過去符合 Google Chrome 規定的先互動過囉 :-) 
  * 當然你的 網站檔案 w80.py 必須配合有首頁的 / 和 /111 以及 /222 等之 route 如下:
@app.route("/")     # for Homepage 首頁 
#@cross_origin("*")     ## 不需要 :-)
def webRoot( ):
   #return '<font color=Red size=7>Hello World!      這是網站喔 !</font>'
   try: 
      return render_template('index.html')  
   except:
      try:
         return app.send_static_file('index.html')  # this file is /static/index.html
      except:
         return "<h1 style='color:red;'>No index.html in templates and static</h1>", 404 

@app.route('/111/')
@app.route('/111')     # 這列可不寫, 但寫了可以避免最後沒打 "/" 會多轉址一次 
def justAuniqueFunction():
    return render_template('haha.html')   # 記得要放 templates 子目錄內 (注意最後有 s)!!!

app.route('/222/')
@app.route('/222')     # 這列可不寫, 但寫了可以避免最後沒打 "/" 會多轉址一次 
def justAunique22228899():
  try:
    return render_template('h2.html')   # 記得要放 templates 子目錄內 (注意最後有 s)!!!
  except: return "<h1 style='color:Red' >找不到 <font color=black>haha.html</font> 喔!</h1>",404
* 使用 return render_template( ) 和 使用 return redirect( ) 各有優缺點
(5)從 Edutalk 抓回的 .py 檔案改為 IoTtalk 的 VP DA 之詳細步驟
 (a)為了以後方便複製修改, 這 DA 改放進子目錄, 例如 b3
    在 static 之下建立子目錄 b3, 把需要的三個檔案先複製進去; 見以下(b)(c)(d)
 (b)去 templates 內把 haha.html 複製到 static\b3\index.html
 (c)去 static 內把 dai.js 複製一份到 static\b3\dai.js 
 (d)假設改好的 VPython 檔案放在 static\b3 內叫做 ball3.py 
  可以先複製之前 Ball-throw2 用的 .py 檔到 static\b3\ball3.py
 (e)把從 Edutalk3 偷回的檔案依據 IoTtalk 的 VPython 規則改好放入 b3 內:
    例如可假裝自己是 Ball-throw1,  
    這樣就去 copy Ball-throw1 的 profile = 開始那四列來用!
   ==> (要注意的就是在 Edutalk 的 df_list 在這邊叫 odf_list)
 (f)注意在 odf_list 的[ ] 內都是函數名稱, 要有對應的函數!! 注意大小寫不同!!!
    (這與 Dummy_Device 的 Python 範例是字串不同! 真Python 和 Vpython 不同:-)
 (g)這時 static\b3 內有三個檔案: index.html, dai.js, ball3.py 
    *修改 index.html, 確定它裡面用到正確的 dan 和 dai:
        /static/dan.js
        ./dai.js      (這裡的 ./ 可以不打; 表示 dai.js 與 HTML 在同個目錄)
    *修改 dai.js, 確定它用到正確的 ball3.py
       fetch_code('ball3.py');    // 這表示 ball3.py 與 HTML 檔案在同一個目錄(不是與 dai.js喔)
 (h)現在即使網站不殺掉重跑 python w80.py
    也可以打 127.0.0.1/static/b3/index.html
  * 這時沒聲音是正常的, 因為"還沒跟網站互動過!", 等從首頁點了跳過來就有聲音了!
 *** 把檔案放入 static 子目錄後, 都不必重跑網站就可以直接使用瀏覽器抓到! 
 (i)改網站檔 w88.py 讓 127.0.0.1/b3  和 127.0.0.1/static/3388.html  也都可以。
    在 w80.py 內加入以下規則:
@app.route('/b3/')
@app.route('/333/')
@app.route('/static/3388.html')
def showBall3FreeFall():
    return redirect("/static/b3/index.html") 
* 注意這時不要用以下這句: (因這樣 current directory 變 /b3 而不是 /static/b3 ) return app.send_static_file('3388.html') (j)你也可以在 static 裡面建立一個 3388.html, 這 3388.html 有兩種做法 (*)用 meta refresh 轉址 去 /static/b3/index.html 不會的用 Google 搜尋 HTML + how + meta + redirect     * 這樣做也很簡單, 只是網址列會被使用者看到轉到 .../static/b3/index.html (*)把 /static/b3/index.html 複製到 /static/3388.html 但這時要修改 3388.html 內 dai.js 那列, 讓它指到 /static/b3/dai.js    *** 注意, 這樣還要修改 dai.js 內指到 .py 檔的那列!      因為在裡面這時所謂的 "這裡(current directory)" 是指 HTML 所在的目錄!      >> 所以要寫 fetch_code('b3/ball3.py'); // 或 如下: fetch_code('/static/b3/ball3.py'); // 前面在 (g)那項也可以這樣寫    *** 絕對路徑相對路徑 各有優缺點 ! (k)剛在(i)內沒有寫 @app.route('/b3') 是 OK 的(/b3 後面沒有斜線), 只是這樣 127.0.0.1/b3 會先抓一次網頁, 轉址為尾巴有斜線的 127.0.0.1/b3/ 然後才根據規則又轉為 127.0.0.1/static/b3/index.html 這在上週(0516)加分題影片內有搭配 curl 命令詳細介紹過!
** 在 youtube 內搜尋 tsaiwn + 0516 + iottalk 就可找到 0516 加分題影片(或把 0526 換成 0523)


   ***  重要注意事項  ***
*檔案編碼(encoding)儘量用 UTF-8; 還有..
 > 必要時用 Notepad 記事本開啟檔案但不要打字立即另存新檔以便改編碼
 > 編輯 Python 檔案儘量用 Python 的 IDLE 修改
   (若用 notepad++修改要注意 TAB ) 
*如果網頁跟你想的不一樣 ..
 > 如果開過網頁又修改了它用的 .py 檔案, 請敲 CTRL_F5 (不是 F5)
   ==> 如這時發現從有聲音變沒聲音是正常的, 因此時等於沒互動直接開啟該頁面!
     這時, 用滑鼠在 VPython 的畫面上點一下, 就會有聲音了(當然你要有播放聲音)
    ==> 如仍覺得怪怪, 先清除瀏覽器cache快取, 再試看看 .. 
 > 清除瀏覽器的 cache (快取) (提醒cache 念如 cash)
 > 善用瀏覽器的 檢查 功能(在網頁空白處按滑鼠右鍵)
 > 檢查各相關檔案編碼(encoding)是否用 utf-8
 > 最後一招: 把相關檔案名稱換掉+重啟(重跑)網站!
*關於如何做好 127.0.0.1/b3 (從EduTalk偷回 code 當 IoTtalk 的 DA)
 v 請參考 https://iottalk.vip/b3/    (哈哈... 就是現在這頁ㄟ) 
 v 也請參考之前網頁 https://iottalk.vip/b1/  

     
Flask 網站出現 GET /favicon.ico HTTP/1.1 [0m" 404 是啥碗糕?
應該很多同學看過或做過 05/16 加分題練習,
阿那應該已經發現(知道)原來 Flask + Python 弄個網站超級簡單!!!
(注意這樣弄之後就不必開啟不安全的瀏覽器就可以用啦!)
而且你這網站全世界都可以用喔! 
如果是 類似 140.113..這種 public IP, 直接給別人就可用;
如果是類似教室的 192.168.. 這種 Private IP, 
那只要抓 ngrok 回來跑一下立即可把你網站變成 https://... 網址也可以給全世界任何人用!
不過! 細心的同學應該會發現你跑網站的 CMD頁面瀏覽紀錄 常出現:
   ... GET /favicon.ico HTTP/1.1 [0m" 404="" ...
這是因為瀏覽器會自動想抓網站的 /favicon.ico
怎辦? 
你可以不理它, 阿可是.. 但是 .. 就是.. 覺得不爽ㄟ 對不對..? 
以下來教大家這個微軟(MicroSoft)發明的 FAVorite ICON 的小秘密..
(1)第一個要解決找不到的問題:
  (a)用小畫家(別懷疑, 小畫家就可以了)建立一個 32x32的圖檔, 存為 myic.png
  (b)把 myic.png 放入你網站的 static 子目錄內; (檔名想也知道可以任意:-)
  (c)在你的 Flask 網站 檔案, 例如 w80.py, 加入以下三列:
@app.route("/favicon.ico")
def helloYouGY( ):
    return send_file('static/myic.png')   # 注意是 static/.. 不是 /static/..
(2)好了, 再也不會有該 404 訊息在你的 跑 python w80.py 的黑黑 CMD 窗 ! 然後開瀏覽器打 127.0.0.1 看看, 在網頁頁籤是否秀出該 myic.png 小圖?   如之前已經瀏覽過或看不到, 敲 CTRL_F5 看看(注意不只 F5)   還有喔..   可是 .. 可是 .. 可是 .. (尼采說很重要所以說三次)   這只有 Chrome 瀏覽器看得到, 其他瀏覽器看不到 icon,   所以建議改用以下(3)較麻煩寫法, 以便除了舊版 IE 之外都看得到該 icon: (3)把剛剛(1)說的在 w80.py 加入那三列換掉改如下六列:
import os
from flask import send_from_directory
@app.route("/favicon.ico")
def helloGiveYYY( ):
   return send_from_directory(os.path.join(app.root_path, 'static'),
             'myic.png', mimetype='image/vnd.microsoft.icon')
(4)好了, 這樣幾乎所有瀏覽器的頁籤瀏覽你這網站首頁(127.0.0.1)都可看到該 icon 了 ! (5)如果其它HTML頁面的頁籤想要顯示不同的 icon 呢? 例如你的 haha.html 想要有不同 icon

   (a)建另一個 32x32 圖片存檔例如 gg38.png 放入你的 static 目錄
   (b)修改你在 templates 內的 haha.html
      在<head>之後立即加入如下:
      <link rel="shortcut icon" href="/static/gg38.png?v=3'>
   (c)存檔後, 重新啟動 python w80.py
     然後測試  127.0.0.1/abc 看看!! (不要問為何 gg38.png 後要加 ?v=3 因為.. 哈哈.. 呵呵..)  

(6)把 haha.html 複製一份到 static 目錄內, 並改名 hey.html (7)建立另一張明顯不同的 32x32 圖片放入 static 目錄內叫 yy49.png (8)修改在 static 內的 hey.html 參考剛(5)(b) 改 <head> 之下那列為: <link rel="shortcut icon" href="/static/yy49.png?v=5'> (9)用瀏覽器測試 (這時不必重跑 python w80.py) 127.0.0.1/static/hey.html 有看到瀏覽器各不同網頁上方頁籤的小 icon 都不一樣嗎 !? 雖然練習這個沒加分 .. 但是 .. 很簡單也很好玩吧!? :-) :-) :-)


台大石明豐教授編寫的 Vpython+Physics 課程文件(pdf,5.28MB)
我改好的自由落體程式碼(假裝成Ball-throw1)等這週三晚上再公佈






.. 好啦.. 提前現在公佈 .. 但再提醒一次:  就是, 請先自己試著改 .. 
==> 至少自己試改十幾分鐘, 若還是弄不出來再看啦 !
    這程式是拿 Edutalk 上的自由落體實驗回來, 改為跟自己電腦上的 Flask + Python 網站配合,
假裝成自己是 Ball-throw1, 這樣就不必自己建立新 DM (Device Model) 就可在 IoTtalk 上使用!!!
在以下程式碼最後有按鈕可以按了複製 :-) 這是假裝成 Ball-throw1 的  自由落體實驗

如果要把 IoTtalk 上的 Ball-throw1 或 Ball-throw2 改為丟入 Edutalk 上用手機控制請點這
      (當然在曾用手機控制後, 也可以在電腦上開啟 Edutalk 的"遙控器"的滑桿控制)
=====>
      往下捲有把 Edutalk1 上王一哲老師的 "自由落下 4-3" 改為..
      改為可放入 自由落體實驗 用手機(或電腦)控制做實驗的程式碼
      DM(Device Model) 的名稱要騙系統是 Ball-Free-Fall   
# Ball-Free-Fall 自由落體實驗  #coding=utf-8   # 配合 IoTtalk -- by tsaiwn@cs.nctu.edu.tw
# 物理參數區  
# 實驗一修改處:以下兩行
height = 20.0     # 初始高度(m)
restitution = 0.9 # 恢復係數

# 模擬實驗參數區
freq = 1000        # 更新頻率(Hz)
dt = 1.0 / freq   # 更新間隔(second)
isr = True  # is_running 偷懶用 isr

# 讓它有聲音, 這次 gj (Good Job) 沒用到
preloadAudio('Startup.wav')
preloadAudio('chord.wav')
preloadAudio('gj.wav')

# 與流程控制有關參數
# 實驗三修改處:改變重力
g = 5.0          # 定義初始重力加速度

# 初始化場景
def scene_init():
    #initial scene 
    global scene, ball, floor, height, label_info
    scene = display(width=800, height=700, center = vector(0, height/2, 0), \
        background=vector(0.5, 0.5, 0))
    floor = box(length=30, height=0.01, width=10, texture=textures.wood )
    ball = sphere(
        pos = vec(0, height, 0), 
        radius = 0.5, 
        color = color.green,
        velocity = vector(0,0,0),
        visible = True
    )
    label_info = label( pos=vec(10,20,0), text='', color = color.yellow)
    myname = label( pos=vec(-10,23,0), text='我是 007008 張大千', \
        color = vector(0,1,51/255), height=28)
    scene.caption = "這是 Version 0.88" # 畫面下方, 方便知道是否抓到新的 .py 檔案 :-)
                
# 當 Radius 旋鈕收到新值時   # 用 Ball-throw1 的 Speed     
def Speed(data):  #Radius(data):
    global ball, isr   # 別忘了 isr 也要 global
    if data != None:
        ball.radius = data[0]
        isr = True # 讓它動
    if ball.radius == 0:  # 秘技 :-)
        ball.velocity.y = 0;
        ball.radius = 5
        ball.pos.y = 33
        isr = False

# 這是原先 自由落體實驗(Ball-Free-Fall) 沒有的 function; # 用 Ball-throw1 的 Height 
def Height(data):
    global ball, isr  # 別忘了 isr 也要 global
    if data != None:
        ball.pos.y = data[0]
        #isr=True   # 先註解掉, 就是改變高度時, 不干涉是否有在動

# 當 Gravity 旋鈕收到新值時  # 用 Ball-throw1 的 Angle 
def Angle(data):  # def Gravity(data):
    global g, isr  # 別忘了 isr 也要 global
    if data != None:
        g = data[0]
        isr=True

# 每秒鐘更新顯示數據  # 改每秒更新 10次 
def update_info():
    global label_info
    label_info.text='gravity重力: {:.2f}\nradius: {:.2f}\nspeed: {:.2f}\nheight: {:.2f}'.\
        format(g,ball.radius,abs(ball.velocity.y),ball.pos.y)
    rate(10, update_info)

# 設定
def setup():
    scene_init()
    profile = {
        'dm_name' : 'Ball-throw1', # 'Ball-Free-Fall',
        'odf_list' :  [Angle, Speed, Height], #[Gravity,Radius],
    }
    dai(profile)
    update_info()
    playAudio('Startup.wav') 

setup()

#scene.autoscale = False   # 看要不要讓場景遠近 AutoScale; 預設是 True
while True:
    # 在每秒重畫 freq 次
    rate(freq)
    if not isr:
        continue  # 回到 while True; 注意 continue 就是繼續下一回合 
    # 計算下一個時間點的資料並將改變畫出
    # 球的位置變化量是 速度 乘上 時間
    ball.pos = ball.pos + ball.velocity * dt
    # 判斷球的新位置是否高於地面
    if ball.pos.y > ball.radius:
        # 如是,依重力加速度修改速度
        ball.velocity.y = ball.velocity.y -g*dt
    else:
        # 如否,依恢復係數計算出反彈後速度,並設定球的位置在地面上
        ball.velocity.y = -ball.velocity.y * restitution
        ball.pos.y = ball.radius
        if abs(ball.velocity.y) < 0.088:  # 速度太小就歸 0 
            ball.velocity.y = 0
            isr = False
        if ball.velocity.y > 0.1:  # 這時播放撞擊聲 
            playAudio('chord.wav')
### End of the Program 自由落體實驗
台大石明豐教授編寫的 Vpython+Physics 課程文件(pdf,5.28MB)




** 點這看 Ball-throw1  (已經參考 Ball-throw2 改為有聲音) 

** 點這看 Ball-throw2  (搭配可在自己電腦上跑的 HTML 網頁和 dai.js) 
  Top  幾個英文字讀音
* 以下改自王一哲老師的自由落下4-3可丟入 EduTalk 的自由落體實驗, 用手機或電腦控制;
    可點這參考這影片(67分鐘) -- 仔細看完影片可讓你程式功力大增 :-)
 
- 
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
這是 從 Edutalk1 王一哲老師 的 自由落下 4-3 抄來, 稍作修改;
    配合 自由落體實驗, 以便用手機控制改 e 或 改半徑
    (當然也可用電腦上遙控器控制; 不過須先用手機掃QRCode控制過後才能用電腦的)
#This 檔案 uses UTF-8 ;;; modified by tsaiwn@cs.nctu.edu.tw
#This 檔案 uses UTF-8    #  coding=utf-8
"""
 VPython教學: 4-3.自由落下, 小球觸地時反彈, 恢復係數為e
 Ver. 1: 2018/2/19
 Ver. 2: 2019/9/6
 作者: 王一哲
"""
from vpython import *        ##  IoTtalk VPDA 要去掉這句 

"""
 1. 參數設定, 設定變數及初始值
"""
size = 1     # 小球半徑
e = 0.9      # 恢復係數
i = 0        # 小球撞地板次數
N = 33       # 小球撞地板次數上限, 到達上限後停止運作
h = 15       # 小球離地高度
g = 9.8      # 重力加速度 9.8 m/s^2
t = 0        # 時間
freq=1000    #  frame rate, 每秒 幾次
dt = 1/freq   # 時間間隔

"""
 2. 畫面設定
"""
scene = canvas(title="Free Fall", width=600, height=425, x=0, y=0, center=vec(0, h/2, 0), background=vec(0, 0.6, 0.6))
floor = box(pos=vec(0, 0, 0), size=vec(40, 0.01, 10), color=color.blue)
ball = sphere(pos=vec(0, h, 0), radius=size, color=color.red, v=vec(0, 0, 0), a=vec(0, -g, 0))

def initGraph( ):
    global gd, yt, vt
    gd = graph(title="plot", width=600, height=450, x=0, y=600, xtitle="t(s)", ytitle="blue: y(m), red: v(m/s)")
    yt = gcurve(graph=gd, color=color.blue)
    vt = gcurve(graph=gd, color=color.red)
label_info = label( pos=vec(10,20,0), text='', color = color.yellow)
et_info = label( pos=vec(-16,20,0), text='', color = vector(255/255,51/255,51/255), background=color.cyan, height=24)
myname = label( pos=vec(-10,-5, -8), text='我是 007008 張大千', \
        color = vector(0,1,51/255), height=24)

initGraph( )
# 當 Radius 旋鈕/滑桿 收到新值時        
def Radius(data):
    global ball, h, i, t, size, isRunning
    if data != None:
        ball.radius = data[0]
        size = data[0] 
        h = 33     # 其實改這 h 沒用 :-)  因 h 只在最開始用到
        ball.pos.y = 33
        i=0
        t=0
        gd.delete( )   # 砍掉 graph
        initGraph( )   # 重生 graph
        isRunning = True
        
# 當 Gravity 旋鈕/滑桿 收到新值時
def Gravity(data):
    global g, h, i, t, e, isRunning
    if data != None:
        e = data[0] / 10
        ball.pos.y = 25
        i=t=0
        gd.delete( )
        initGraph( )
        isRunning = True
        
def update_info():
    global label_info, e,i    # 其實這些 global 都不必寫
    label_info.text='gravity重力: {:.2f}\nradius: {:.2f}\nspeed: {:.2f}\nheight: {:.2f}'.\
        format(g, ball.radius, ball.v.y, ball.pos.y)
    et_info.text='恢復係數 e: {:.3f}\nTouch: {:d}\n'.format(e, i)
    rate(5, update_info)   # 每秒更新 5 次

# 設定
def setup():   
    # scene_init()    # 習慣上應該把初始化場景寫成 function
    profile = {
        'dm_name' : 'Ball-Free-Fall',
        'df_list' : [Gravity, Radius],     # 注意是 df_list; 在 IoTtalk 是 odf_list
    }
    dai(profile)   # connect ro Edutalk   ( or  IoTtalk server ) 
    update_info()
    playAudio('Startup.wav') 
setup()

"""
 3. 物體運動部分, 小球觸地時反彈
"""
isRunning = True
while True:   # (i < N):
    rate(freq)
    while not isRunning:     ## 此處用 while 比用 if 好 
       sleep(0.2)   ## 這是 VPython 的 sleep( ) 
       continue   # go back to while
    ball.v += ball.a*dt
    ball.pos += ball.v*dt
    yt.plot(pos=(t, ball.pos.y))
    vt.plot(pos=(t, ball.v.y))
    if(ball.pos.y - floor.pos.y <= size +0.5*floor.height  and ball.v.y < 0):
        ball.v.y = -ball.v.y*e
        i += 1
        playAudio("chord.wav")
    t += dt
    if i >= N:
        isRunning = False

#print("i = ", i)
#print("t = ", t)

  * 上述程式碼可丟入 EduTalk 的自由落體實驗, 用手機或電腦控制; 可點這參考這影片(67分鐘)
  * 王一哲老師部落格 (一個地球人在臺北、台北)     王一哲老師VPython 教學文件 @ Github
* 南港高中 高慧君老師 VPhysics 課程教材(Python 2.7 + VPython 6)
   http://drweb.nksh.tp.edu.tw/student/lessons/F/
    (請注意: Python 2.xx 和 Python 3.xx 的語法有些許差異)

* 南港高中 高慧君老師 Turtle Graph 課程教材(Python 2.7 + VPython 6)
   http://drweb.nksh.tp.edu.tw/student/lessons/A/  
    (好像..只寫了九分之一) 
     
GlowScript.org 上 王一哲老師的蛇擺   蛇擺教材 點這觀賞並看解說
            以及 王一哲老師的單擺教材(含不考慮與考慮有空氣阻力)
建中曾靖夫老師的蛇擺教材
Simulation of Pendulum: VPython Tutorial 3 (Visual Python)
Damping Force(阻尼力;減振力)(International Edition University Physics, Book.1984)
*王一哲老師的蛇擺(許多單擺): https://hackmd.io/@yizhewang/SyEWFOEmm
* 以下改自王一哲老師的蛇擺程式碼可丟入 EduTalk 的自由落體實驗, 用手機或電腦控制;
    多研究別人程式(這程式用到 Python 的 class), 可讓你程式功力大增 :-)
* 可以到 GlowScript.org 觀摩更多 VPython 範例 (註冊後可以上傳程式碼)
 
- 
 
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219

這是 從 王一哲老師 的 蛇擺程式 抄來, 稍作修改;
    配合 自由落體實驗, 以便用手機控制改 num 幾個單擺 或 改 N 最長的每週期擺幾次
   
(當然手機掃瞄QRCode控制過後, 也可用電腦上遙控器控制)
#This 檔案 uses coding: utf-8 ;;; modified by tsaiwn@cs.nctu.edu.tw
## #** 王一哲老師的蛇擺放入 Edutalk 的自由落體實驗, 可用手機控制參數 num 和 N
"""
VPython教學進階內容: 蛇擺 pendulum wave, 使用 class
 Ver. 1: 2018/7/12
 Ver. 2: 2018/7/14 加上計時器
 作者: 王一哲
"""
from vpython import *        ##  IoTtalk VPDA 要去掉這句 

## whn run in GlowScript or /withOUT EDUtalk, enable next 2 functions
# def playAudio(gg): return   # for run without Edutalk/IoTtalk
# def dai(gg): return   # for run without Edutalk/IoTtalk

"""
 1. 參數設定, 設定變數及初始值
"""
num = 25            # 單擺的個數
N = 15              # 週期最長的單擺在蛇擺的1個週期內擺動的次數
rc = 10    # [0, 999]  # 模仿阻尼係素 ; 設 0 就當作真空不會停止;; Damping Force Factor
## 以上當作可改的參數, 配合 DM in Edutalk and/or IoTtalk
Tpw = 30         # 蛇擺的週期
angle = 30  # 起始擺角, Degree, 度 ; 也可以改變 Tpw 和 angle 看看
##
stopPoint = radians(0.1)   #  stop when smaller than 0.1 degree
isRunning = False  

freq = 1000
dt =  1/freq         # 時間間隔
scaleRc = 0.0005    # 調整 rc  模擬阻尼係素
dfFactor = 1 - scaleRc * rc    # 真正用這乘, 相當於恢復係素 ; damping force Factor
# dfFactor = 0.9975   #  1.0 表示沒有空氣阻力 
theta000 = radians(angle) # 起始擺角, 用 radians 將單位換成 rad
theta0 = theta000   # theta0 會被 Damping force 改變
g = 9.8             # 重力加速度
Tmax = Tpw / N      # 週期最長的單擺擺動週期
Lmax = Tmax**2 * g / (4 * pi**2)   # 週期最長的單擺擺長 
width = Lmax        # 將畫面邊長設定為最長的單擺擺長
m = 1               # 小球質量
size = width / (2 * num)           # 小球半徑
i = 0               # 小球經過週期次數
t = 0               # 時間

def changeRc(gg):
    global rc, dfFactor
    rc = int(gg)
    if rc > 999: rc = 999    # 避免太快停掉
    if rc < 0: rc = 0    # 防呆
    dfFactor = 1 - scaleRc * rc    # 真正用這乘, 相當於恢復係素

"""
 2. 畫面設定
"""
# 產生動畫視窗、天花板
scene = canvas(title="Pendulum Wave", width=600, height=600, x=0, y=0, background=color.black)
scene.camera.pos = vec(-1.5*width, 0.5*width, width)
scene.camera.axis = vec(1.5*width, -0.5*width, -width)
roof = box(pos=vec(0, (width + size)/2, 0), size=vec(width, size, 0.5*width), color=color.cyan)
timer = label(pos=vec(-0.58*width, 0.77*width, 0), text="t =  s", space=50, height=24,
              border=4, font="monospace")
para_info = label(pos=vec(0.9*width, 0.95*width, 0), space=30, height=24, 
    text="單擺的個數 num:\n最長單擺擺動次數 N:", color=color.yellow)
th0_info = label(pos=vec(-0.6*width, 0.65*width, 0), space=30, height=18, 
    text="theta0: ", color=color.yellow)

# 新增類別 Pendulum, 輸入週期T, 懸掛位置loc, 編號idx, 自動產生小球及對應的繩子
# 設定方法 update, 輸入經過的時間dt, 更新單擺狀態
class Pendulum:   
    def  delBR(self):   # delete inner components
        self.ball.visible = False
        self.rope.visible = False
        del self.ball
        del self.rope  

    def __init__(self, T, loc, idx):
        self.T = T
        self.loc = loc
        self.idx = idx
        self.L = self.T**2 * g / (4 * pi**2)
        self.I = m * self.L**2
        self.alpha = 0
        self.omega = 0
        self.theta = theta0
        self.ball = sphere(pos=vec(self.loc, width/2 - self.L*cos(theta0), self.L*sin(theta0)), 
                           radius=size, color=vec(1 - self.idx/num, 0, self.idx/num))
        self.rope = cylinder(pos=vec(self.loc, width/2, 0), axis=self.ball.pos - 
             vec(self.loc, width/2, 0),
                             radius=0.1*size, color=color.yellow)
    def update(self, dt):
        global theta0, isRunning
        if not isRunning:
            return    ## 防呆, 因此時可能 pendulum 物件已經被砍了正要重生
        self.dt = dt
        ## self.alpha = -m*g*self.ball.pos.z/self.I   # angular acceleration
        self.alpha = -g/self.L * sin(self.theta)    # angular acceleration
        pomg = self.omega;   # previous omega; 之前速度; 往右 > 0; 往左小於 0
        self.omega += self.alpha*self.dt   ## velocity
        if self.idx==0 and pomg * self.omega < 0:    # velocity changing positive / negative
            theta0 *= dfFactor    # #模擬受到空氣阻力 (阻尼減振)
        # self.theta += self.omega*self.dt
        gg = self.theta + self.omega*self.dt
        if gg > 0 and gg >= theta0:    # 模擬阻尼減振, 並防止因誤差導致越盪越高 !
            pass    #  # 預留萬一想把以下該列註解掉 :-) 
            gg = theta0
        if -gg > 0 and -gg >= theta0:
            pass
            gg = -theta0
        self.theta = gg
        self.ball.pos = vec(self.loc, 0.5*width - self.L*cos(self.theta), self.L*sin(self.theta))
        self.rope.axis = self.ball.pos - vec(self.loc, 0.5*width, 0)
        if theta0 < stopPoint: isRunning = False

def initPens( ):
  global pendulums
# 利用自訂類別 Pendulum 產生 num 個單擺
  pendulums = []
  for i in range(num):
    T = Tpw / (N + i)
    ddd=num
    if ddd < 6: ddd = 6     # prevent from  divided by 0 when num == 1
    loc = width*(-0.5+(i/(ddd-1)))   ## ddd-1 : at least has 5 NOW
    pendulum = Pendulum(T, loc, i)
    pendulums.append(pendulum)

def resetGame( ):
    global t, theta0, isRunning
    isRunning = False
    ## Edutalk VPython 在 main thread 之外 不能用 sleep( ) ???
    # sleep(0.3)    # let main thread do something else
    for pendulum in pendulums:
        pendulum.delBR( )   ## 砍掉內部物件 ball, rope; VPython 不會立即做 __del__(self)
        ## 主要因為根據手冊, __del__(self) 要等到記憶體被回收才會執行 ! 
        del pendulum      ## delete the object 
    #import gc     # Edutalk VPython 不能用
    #gc.collect()
    t = 0
    theta0 = theta000    # initial theta
    initPens( )   # generate all pendulum and put them in pendulums
    isRunning = True
    playAudio("Go.wav")

# 當 Gravity 旋鈕/滑桿 收到新值時 # 參數改變 
def Gravity(data):
    global num, isRunning     ## 單擺的個數 num
    if data != None:
        num = int( data[0] * 5 )   # [0, 10] == to [0, 50]
        if num < 1: num = 1
        resetGame( )

# 當 Radius 旋鈕/滑桿 收到新值時         # 參數改變 
def Radius(data):
    global N, isRunning    ## N :  最長單擺擺動次數 (一個單擺週期內)
    if data != None:
        N = int( data[0] * 3 )    # [0, 10] == to [0, 30]
        if N < 3: N = 3
        resetGame( )

# 當 Speed 旋鈕/滑桿 收到新值時   # 參數改變 (先寫好備用 :-)
def Speed(data):
    global rc, isRunning    ## rc :  模仿阻尼係素 [0, 999]
    if data != None:
        changeRc( data[0] )   # do  rc = data[0]
        resetGame( )
## 
upc = 0
def update_info( ):
    global upc   # update counter
    upc += 1
    if upc < int(freq/6) : return   # 每秒更新 6 次 ; 注意Python 2  不認識 freq//6
    upc = 0   # reset counter
    #content = "t = " + str(int(t)) + " s"
    timer.text = "t = {:.1f} s".format(t)
    para_info.text =  \
    "單擺的個數 num = {:d}\n最長單擺擺動次數 N: {:d}\n模擬阻尼係素(0~99): {:d}".format(num, N, rc)
    th0_info.text  =  "Theta0: {:.5f}".format(theta0)
    # rate(6, update_info)   # 每秒更新 6 次# 單機版不能用
#
def setup( ):
    profile = {
        'dm_name' : 'Ball-Free-Fall',
        'df_list' : [Gravity, Radius],     # 注意是 df_list; 在 IoTtalk 是 odf_list
    }
    dai(profile)   # connect ro Edutalk   ( or  IoTtalk server ) 
    update_info( )
    playAudio("Go.wav")
###
gotInput = False
theInput = "xxx"
allDead = False
def checkInput( ):
    global gotInput, allDead
    if not gotInput:
        return     ## usually do read by other thread
    if theInput =='quit' or theInput == "exit":
        allDead = True
        return
    # check keyboard input
    gotInput = False   # indicate we took theInput and handled it
    # check DAN.pull ?  or use other thread to do so

"""
 3. 物體運動部分
"""
initPens( )
setup( )
##        # Start your other threads here !
isRunning = True
while True:
    rate(freq)  # # 相當於 sleep(1/freq) 
    while not isRunning:   # wait till True
        checkInput( )   # to see if we got input
        sleep(0.2)   # 這是 VPython 的 sleep( )
        continue     # go back to while not isRunning 
    for pendulum in pendulums:
        pendulum.update(dt)
    t += dt
    update_info( )
    checkInput( )   # to see if we got input
    if allDead: break;
* 前面王一哲老師的 Pendulum class 類別裡面用到了外部的變數如 width, g, num 等等,
    其實是很不好的習慣, 因為這樣一來, 它就很難變成一個模組放另一個檔案!!
  ==> 不過, 反正在 Edutalk 和 IoTtalk 的 VPython Device 都只能寫成一個檔案 !
    以下是改寫後的版本: 除了 pi 之外都沒用外部變數了!
    當然, 這樣使用時也不太一樣 !
#filename: pen.py  (故意) -- pendulum.py -- coding: utf-8  
# Author:  中和高中王一哲老師;  modified by tsaiwn@cs.nctu.edu.tw

from vpython import *        ##  IoTtalk VPDA 要去掉這句 

# 新增類別 Pendulum, 輸入週期T, 懸掛位置loc, 編號idx, 自動產生小球及對應的繩子
# 設定方法 update, 輸入經過的時間dt, 更新單擺狀態
# 使用此 Pendulum 的範例在後面 
class Pendulum:   
    __theta0 = radians(10);   ## private shared variable; default 10 degree(故意)
    __dfFactor = 1;    ## 左邊寫 __ 表示 private 藏起來; 要透過函數才可存取
    g = 9.8    ## 像這 g 則可從外部打 Pendulum.g = 33.88 改掉
    m = 1      ## 前面這幾個是 class 變數, 不是 instance 變數, 表示大家共用
    # pi = 3.14159      # pi 在 vpython 模組內; 單機版記得要 from vpython import *
    def getTheta0( ):   #getter
        return Pendulum.__theta0  
    def setTha(arg):    # setter  #故意, 應該叫 setTheta0(arg) 比較好
        Pendulum.__theta0 = arg
    def setDfFactor(arg):     # set Damping force Factor (coefficient)
        Pendulum.__dfFactor = arg
    def  delBR(self):   # delete inner components  ;  __del(self)__ 
        self.ball.visible = False
        self.rope.visible = False
        del self.ball
        del self.rope  
    def __init__(self, T, loc, idx, num, width, size):
        self.T = T
        self.loc = loc
        self.idx = idx
        self.width = width
        self.L = self.T**2 * Pendulum.g / (4 * pi**2)
        self.I = Pendulum.m * self.L**2
        self.alpha = 0
        self.omega = 0
        self.theta = Pendulum.__theta0
        self.ball = sphere(pos=vec(self.loc, width/2 - self.L*cos(Pendulum.__theta0),
                        self.L*sin(Pendulum.__theta0)), 
                        radius=size, color=vec(1 - self.idx/num, 0, self.idx/num))
        self.rope = cylinder(pos=vec(self.loc, width/2, 0), 
                       axis=self.ball.pos - vec(self.loc, width/2, 0),
                       radius=0.1*size, color=color.yellow)
    def update(self, dt):
        self.dt = dt
        ##self.alpha = -Pendulum.m*Pendulum.g*self.ball.pos.z/self.I   # angular acceleration
        self.alpha = -Pendulum.g/self.L * sin(self.theta)    # angular acceleration
        pomg = self.omega;   ## 之前速度; 往右 > 0; 往左小於 0
        self.omega += self.alpha*self.dt   ## velocity
        if self.idx==0 and pomg * self.omega < 0:   ##  negative / positive velocity
            Pendulum.__theta0 *= Pendulum.__dfFactor
        # self.theta += self.omega*self.dt
        gg = self.theta + self.omega*self.dt
        if gg > 0 and gg > Pendulum.__theta0:    # 防止因誤差導致越盪越高 !
            pass
            gg = Pendulum.__theta0
        if -gg > 0 and -gg > Pendulum.__theta0:
            pass
            gg =  - Pendulum.__theta0
        self.theta = gg
        self.ball.pos = vec(self.loc, 0.5*self.width - 
             self.L*cos(self.theta), self.L*sin(self.theta))
        self.rope.axis = self.ball.pos - vec(self.loc, 0.5*self.width, 0)
## 使用 Pendulum 範例;  單機版要 from pen import *      # 匯入 pen.py 全部
## 假設 theta0, dfFactor, stopPoint, T, loc, i, num, width, size 都已經有了
    Pendulum.setTha(theta0)      # 注意函數名稱
    Pendulum.setDfFactor(dfFactor)   # 透過 setter 寫入共用的 dfFactor
    Pendulum.g = g    ## 因為 g 左邊沒 __ 所以不必用 setter 函數就可直接改
    Pendulum.m = m    ## m 和 g 都是類別共用變數; 就是所有單擺這類別的物件共用的變數。
    ## Pendulum.pi = pi     # 用外部的 (在 vpython 內) pi 所以不要自己定義
    pendulum = Pendulum(T, loc, i ,  num, width, size)    ## 生出一個單擺放入變數 pendulum 
    # 必要時可讀取 Pendulum 內的 theta0 如下
    ans = Pendulum.getTheta0( )    # getTheta0( ) 可用來與 stopPoint 比較
*** 05/15加分題詳細可以 點這看影片說明 (善用影片下方說明與子題連結)
CMoreTV:  https://play.google.com/store/search?q=cmoretv

** demo.iottalk.tw「分享」按鈕- 社交外掛程式 - Facebook for Developers

請按  幫忙分享喔 !
有時候看起來對的其實是錯的!(我很久以前寫的網誌:-) * 不能反攻大陸的真正理由!(也是以前寫的, 其實是給大一學生的習題:-) * 何以1752年9月只有19天 ? 參考 Unix 的 cal 命令 ..(也是大一習題:-) * 為什麼中國(清朝)的 1911年 12 月只有 18 天 ?!(還是以前寫的:-) * [茶餘飯後] 魑魅魍魎 鬼話連篇(大一計概介紹802.11無線網路:-) * [很小的數]何處惹塵埃? 沙漠中是塵埃!(大一計概:-) * [很大的數]再談關於 Google 與 Googol(大一計概:-) * [也是很大的數]佛曰不可說不可說 比一個 googol 還大!(大一計概:-) * 物聯網、IoTtalk、薑黃、免疫力...(兩個月前寫的:-) * 請注意..感冒其實沒有藥     大家要顧好身體, 否則沒機會實現你的創意!我也有粉絲專頁ㄟ:-) 呵呵 (據說 "呵呵" 是北宋蘇東坡的口頭禪 :-)
你不懂的內容農場取妻當娶李子柒? 7 min.   ( 館長也哈她  ) * 台灣百大 Youtuber 收入排行全球10大最高收入华人YouTuber, 2019.1203, by 小楠ngrok 讓外部能夠連到 Intranet 內 Localhost的網站及服務Facebook Messenger Bot 範例 https://developers.facebook.com/docs/messenger-platform/getting-started/sample-apps/?translation用Python 寫一個 fb Messenger Bot手把手教你搭建 LINE 聊天機器人LineBot+Python,輕鬆建立聊天機器人(北科大程式設計研究社) * https://developers.line.biz/en/services/messaging-api/(LINE 關方網站教學文件) * 防詐騙、防詐騙、防詐騙 南寧詐騙案全台灣有 25萬人被騙 !小心詐騙 !小心詐騙 !小心詐騙 !投資自己,讓今天的自己 比 昨天的自己 更值錢 ! ( 建議每天 15分鐘在 voiceTube.tw ) *
 
** 關於在自己電腦上跑的 Bulb
(一)參考 之前 影片: 0324 P2 -- IoTtalk平台介紹與使用(二) - Part 2/5 自己電腦上的 Bulb
     (Part2 是 Bulb, Part5 則是 Dummy Device 在 IoTtalk 物聯網平台入門手冊的(五)那項)

(二)讓 你的 Bulb 變成你的 Flask 網站的一部分
   (1)把你之前做過的 Bulb 整個目錄複製到 你網站的 static 目錄之下, 假設叫 bulb
   (2)這樣就可以用 127.0.0.1/static/bulb/index.html 使用你的 Bulb 了(當然網站要跑著)
   (3)如要讓使用者打 127.0.0.1/bulb 就可以用, 
      修改你的網站 .py檔案加入以下三列後重啟網站即可:
@app.route('/bulb/')
def giveYouABulb():
    return redirect("/static/bulb/index.html")  
(三)讓 你的 Bulb 一開始顏色跟別人不一樣 在你的 Bulb 內 js\ida.js 大約 LINE 9, 10, 11 有開始時 r, g, b 的初值, 改成你喜歡的即可! (四)讓 你的 Bulb 在接收到第三個值之後, 開始亂變顏色 ==> 繼續修改 ida.js (1)在 LINE 13 處增加三個變數:
var firstRun = 1;  // 程式的狀態
var tout = 0; // time out 時間 -- 過多少 ms 要執行 ccc( ) 變化顏色
var myLoop;   // 用來記住定時工作的工作代碼 (job ID)
(2)在大約 LINE 27 函數 function Luminance (data) { 內加入以下四列:
if(firstRun==99)if(data[0] < 1) { firstRun=1; clearTimeout(myLoop); }
if(firstRun==3){ firstRun=99; myLoop=setTimeout(ccc, 3000); } /// 三秒後啟動 ccc( )
if(firstRun==2)firstRun=3; 
if(firstRun==1)firstRun=2; 
// 說明:四列依序..
// 如果在有變化顏色狀態收到 0 立即取消定時 ccc( ) 工作, 且把 firstRun 設為 1
// 如果 firstRun 是 3, 把 firstRun 改為 99; 並且 設定過 3 秒後執行 ccc( ) 變化顏色
// 如果 firstRun 是 2, 把 firstRun 改為 3
// 如果 firstRun 是 1, 把 firstRun 改為 2
/// 這種 1 => 2 ==> 3 ==> 99 的做法叫做 finite state machine 有限狀態機
(3)緊接著在 Luminance 函數結束後另一個函數之前, 塞入以下新函數 ccc( ) :
function ccc( ) {
    r = Math.floor(256 * Math.random( ));
    g = Math.floor(256 * Math.random( ));
    b = Math.floor(256 * Math.random( ));
    if( Math.max(r, g, b) < 102) { r=g=255; b=51;  }
    tout = 150 + 100 * Math.floor( 20* Math.random( ));
    myLoop = setTimeout(ccc, tout);
    draw();
} 
// 函數說明:
// 亂數決定 r, g, b ; 如果 r, g, b 都小於 102 則改為 rgb(255, 255, 51) 黃色
// 亂數決定 tout, 在 (150, 250, 350, .. 2050) 中挑一個
// 設定過 tout 微秒(milli second)後執行 ccc( ), 但記住工作代號在 myLoop 這變數
// 立即 draw( ) 更新燈泡
// 至於 myLoop 工作代號是要用於接收到 0 時停止 ccc( ) 表示不再變換顏色, 參考(2)
(五)順便讓 註冊到的 伺服器名稱顯示在你的 Bulb 下方: (1) 修改 index.html 在最後一個 </div> (倒數第三列)之前加入以下兩列:
<font color=red size=4>connect to Server: </font> &nbsp;
<b id="ggyy" style="font-size:2vw;color:yellow"> Unknown </b>
(2) 修改 ida.js 在函數 ida_init ( ) {  的函數內 加入以下這列:
$('#ggyy')[0].innerText = csmapi.get_endpoint() ;
==> 這意思是把註冊的伺服器名稱塞入 index.html 內 id 是 "ggyy" 的文字部分! * 好了, 測試看看並稍為研究一下為何這樣在傳入三個值後過三秒就會開始亂變顏色 :-) * 給它輸入 0 之後會怎樣? Q: 為何穿過 ngrok 後燈泡無法註冊成功 ? A: 可能你在 Bulb 的 ida.js 的 LINE 2 那列指定 Server 時沒用 https, 要改為用 https:// 類似如下: csmapi.set_endpoint ('https://demo.iottalk.tw'); // 請用你被指定的 Server **以下是透過 ngrok 提供的 https:// 網址連到我電腦上的燈泡: ** ngrok 可到 ngrok.com 去下載, 解壓縮後, 假設你已經跑了網站; 打 ngrok http 80 即可挖地道(Tunnel)連到你的 port 80 (沒註冊每次網站可活著 8 小時, 有註冊可活到你砍了它; 有付錢可以指定網址名稱)

2020.0523 Bonus 加分題題目備忘 Flask + Python + Edutalk的 code 回來做 IoTtalk 的 VPython DA
2020.05.23 加分題..
再多看一眼(多練一下) VPython 的 DA  -- 還是加分題 -- 
* 這次加分題也很簡單, 主要是讓大家更清楚 VPython 的 DA 要如何寫
   (物理沒念好也OK, 網路上偷別人的來改即可:-)
* 總共分六部分(當然要越來越多啊不然怎加分呢:-)
Due: 2020/05/31(日)23:59:59  (給大家多一點時間:-)
(1)把上次 Flask+Python 改為 127.0.0.1/111 就會跑 Ball-Throw1;
   參考   https://iottalk.vip/b1/      
   (上次的沒做也沒關係, 我會先花大約三分半鐘 Demo 這部分;
      別懷疑, 如不求甚解真的只需三分鐘左右)! !
(2)把會跑 Ball-throw2 的 HTML 檔案放入 templates 目錄, 
   使得 127.0.0.1/222 會跑 Ball-throw2 (大約兩分鐘即可:-)
(3)到 https://edutalk3.nctu.edu.tw 用你手機控制(a)行星運動, 
   和(b)自由落體; 
   這部分要各拍一張照片, 看得到你拿著手機在控制, 
   共兩張照片 和兩張截圖, 截圖時是用你電腦控制(週日影片會Demo);
(4)把 Ball-throw2 稍微修改丟入 edutalk3, 可以丟入"行星運動", 
   也可以丟入"自由落體";
    不過丟入 "行星運動" 的 code 我已公布在 iottalk.vip/b1 內; 
    這部分也是和(3)類似拍一張照片和一張截圖!
(5)類似(4), 只是改用 Ball-throw1, 
   這個改好可配合 edutalk3 的 code 也已經在 iottalk.vip/b1 內;
    這部分也是和(3)和(4)類似拍一張照片和一張截圖證明你有練過:-)
(6)偷取 Edutalk3 的一個 .py 程式碼回來自己網站跑
   在 edutalk3 內, 若各程式碼改爛了可點畫面的 Reset 恢復 !
   把原先自由落體(或行星運動也可)的 .py 程式碼偷回自己電腦稍微修改一點點, 以便配合 IoTtalk server,
   讓你的網站 127.0.0.1/333 和 127.0.0.1/static/3388.html 都可讓你用 RemoteControl 控制自由落體(或行星運動也可)!
   這題特別規定: 
     這/333 的HTML檔案不可以放 templates 內, 
   要和其它 dai.js, dan.js, 自由落體的.py 都一起放 static 內(可建子目錄)
   關於在 static 子目錄內各 Vpython DA, 雖然最好再建立子目錄, 但是..
   因為我們目前只要跑三個 VPython 的 DA, 所以通通放 static 內不建立子目錄也 OK!
   只是要注意各 DA 用到的檔名不要互相搞錯 !
(7)繳交:  當然就是心得 + 圖片 + 必要時對圖片簡單解說); 不必拍攝影片!
     Due: 2020/05/31(日)23:59:59  
(a) 心得至少 88字, 至多 388字; (加分題要再次多一點心得:-)
(b) 除前面規定照片和截圖,
全部測試過程照片和截圖, 總共至少 15張, 至多 20張
還要再說一次: 卡通小美人魚裡有經典名句:
   Life is Full of Choices. You can NOT have it all.
因為是加分題,
其他功課較忙的可以不必繳交這題:-)
建議還是要看看影片以及同學心得!
0523加分題的參考影片在這:(善用影片下方的子題時間連結) 
      https://youtu.be/zDfoH9MnKg4
除了最後一項沒詳細步驟, 其它前五項在影片中都有詳細Demo與解說!
至於(6)最後一項, 其實也很簡單,
就是從Edutalk抓回 .py 檔案後先改為符合 IoTtalk 的 VPython DA 規定!
其實 Edutalk 和 IoTtalk 只差在 profile 用 df_list 或是 odf_list,
當然如不自己建立新 DM 則須配合現存的 DM 用其 dm_nameodf_list,
要確定 odf_list 內各 ODF 都有對應的函數(多看幾個範例就會了:-)
然後要把 dai.js 改為用到正確的 .py 檔(也注意 dai.js 大約 LINE5 是否用到正確 IoTtalk server);
還有 HTML 檔案要用到正確的 dai.js 和 dan.js
最後, 網站檔(w80.py)增加規則並利用 return redirect("你的網址") 轉到正確網頁!
如果這樣還是不太懂, 詳細步驟請看這網頁 iottalk.vip/b3 !
但是,
最後(6)我改好的自由落體程式碼(假裝成Ball-throw1),
等這週三(05/27)晚上再公佈於該 /b3 網頁:-)
[0527更新] 補充影片在這: https://youtu.be/-mluwqDy4f0

 
*** 精簡版 Flask + Python + IoTtalk DA
這次最大不同是: 我們希望除了 VPython 的 .py 檔之外全部共用 !!!
    就是說,包括 dai.js, dan.js, csmapi.js, 以及一大堆不是我們寫的程式庫都共用。
這樣跟 IoTtalk server 差別只剩下 它的 VPython Device List 是程式根據目錄內 .py 檔生出的,
但我們的是手動寫在首頁 index.html 裡面 :-)
當然, 要注意 dai.js 兩個地方:
      (a) 是否連到正確 IoTtalk server ? 在大約 LINE 5 的地方!
      (b) 是否會指到正確的 .py 檔; 在大約 LINE 132 (這句現在不可以寫死!)
          (就是說會根據 dai.js 的 LINE2, 但要指到我們自己電腦上 .py 的目錄)
(一)啟動虛擬環境(沒必要每次新建隔離環境); 進入網站根目錄, 準備網站所需要的:
    * 建立子目錄 static 和 templates   (注意有 s)
    * 建立 網站檔案 w90.py 如下:  (編碼請用 utf-8)
#This 檔案 w90.py  uses UTF-8  ## coding=utf-8   
myPort = 80
## 請在這檔案的目錄建立兩個子目錄: static 和 templates   (注意最後有 s)
from flask import Flask, request
from flask import render_template, redirect, send_file

app =Flask(__name__)

import socket
myhost = socket.gethostname()     # myhost 要給 HTML 內的 myname (故意)
myip = socket.gethostbyname(myhost)    # 通常名字與 HTML 內相同比較不會搞混 ! 

@app.route("/")
def webRoot( ):
   try:     # 注意 index.html 內要用的是 {{myname}}  和 {{myip}} 
      # return render_template('index.html')  ## 如果沒用到 Jinja2 template
      return render_template('index.html', myname=myhost, myip=myip) 
   except:
      return "<h1 style='color:red;'>No index.html in templates.</h1>", 404 

import os
from flask import send_from_directory
@app.route("/favicon.ico")
def helloGiveIC( ):
   try:
       return send_from_directory(os.path.join(app.root_path, 'static/img'),
             'myicon.png', mimetype='image/vnd.microsoft.icon')
   except: 
       return "No /static/img/myicon.png", 403  #故意用 403 (FORBIDDEN)

# 注意瀏覽器的網址列要用 127.0.0.1 或你的 IP,  不是打 0.0.0.0 喔! 後面打 :port
app.run( '0.0.0.0' , port=myPort, debug=True)   # 如 port 是 80 則可不用打 :80
(二)建立首頁 Homepage, 在 templates 子目錄內建立 index.html (編碼請用 utf-8) * 以下這 HTML 首頁內紅色字體render_template( ) 會傳入的兩個變數
<!DOCTYPE html> <!--- 編碼用 utf-8; use utf-8, Put this file in /templates/index.html  ----->
<html>
<head>
 <title>My VP DA on my Flask Web site </title>
 </head>
<body style="margin-left:2vw;background:lightcyan;">
<b><font color=blue size=5><font color=red 
   size=7>My Cyber / VP DA List -- {{myname}} </font>
<img width="128" align="left" 
  src="https://thumbs.gfycat.com/DistantOrangeIndianpalmsquirrel-size_restricted.gif" >
  <! https://thumbs.gfycat.com/DistantOrangeIndianpalmsquirrel-size_restricted.gif >
  <! https://i.gifer.com/Z8Dq.gif >
  <! https://media.giphy.com/media/3JP7cuyaPh2UiDFlwD/giphy.gif >
  <! http://clipart-library.com/images/BTaropdpc.gif  >
  <! https://i.gifer.com/Z8Dq.gif >
<b><pre><font color=darkred size=6>Cyber Devices List ({{myip}}) </font>
 <a target=_blank href="/static/bulb/index.html">我的 Bulb 燈泡</a>
 <a target=_blank href="/static/bulb2/index.html">Bulb2 會<font color=red
   >自動</font><font color=darkGreen size=6>變色</font>的 Bulb 燈泡</a>
<hr style="border-top: 1px; solid red;width:66vw;" />
<font color=darkGreen size=6
     >VPython Devices List</font>
 <a target=_blank href="/static/vp/index.html#Ball-throw1">Ball-throw1 丟球, 三個參數</a>
 <a target=_blank href="/static/vp/index.html#Ball-throw2">Ball-throw2 丟球, 一個參數</a>
 <a target=_blank href="/static/vp/index.html#Free-fall">自由落體-裝Ball-throw1,三參數)</a>
 <a target=_blank href="/static/vp/index.html#V_Dummy">Dummy_Device, 要有 V_Dummy.py</a>
 <a target=_blank href="/static/vp/index.html#haha">用到檔案 haha.py</a>

<font color=red>*請把 .py 檔案放 /static/vp/py/ 內; 注意 dai.js 要抓這裡面的 .py </font
 ></font></pre>
<h2 style="color:orange;">====== ====== ====== ====== ===== ======</h2>
<b style="color:blue;font-size:24px;">
<a target=_blank href="https://www.cwb.gov.tw/V8/C/">中央氣象局</a> <br/>
<a target=_blank href="https://www.cwb.gov.tw/V8/C/W/OBS_Station.html?ID=C0D66">新竹市氣象站</a>
<br/>
<a target=_blank href="https://www.cwb.gov.tw/V8/C/W/OBS_Station.html?ID=C0D58">新竹寶山氣象站</a>
<br/>
<a target=_blank href="https://www.cwb.gov.tw/V8/C/W/OBS_Station.html?ID=72D08">新竹五峰氣象站</a>
<br/>
<a target=_blank href="https://www.cwb.gov.tw/V8/C/W/OBS_Station.html?ID=46741">台南氣象站</a>
 <div style="background:red;height:3px;width:58vw;"></div>
 <div style="background:green;height:3px;width:63vw;margin-top:2px;"></div>
<img width="128" align="left" 
  src="https://i.pinimg.com/originals/b2/c7/f2/b2c7f2a644b94acf26b05ce6245be311.gif" 
  >  <img width="128" align="absMiddle"
  src="https://thumbs.gfycat.com/BasicBothFish-small.gif">
 <div style="background:blue;height:3px;width:58vw;margin-top:2px;"></div>
</body>
</html>

** 關於使用 Jinja2 Template 入門, 請參考這以前提醒過的 w3.py(用到兩個檔案) (三)生出(複製來放入) Cyber Devices Bulb 和 Bulb2  (1)把以前做好的 Bulb 整個目錄複製到 /static 並改名 bulb  (2)把 bulb 整個目錄複製一份 改名 bulb2, 修改一下讓它收到三個資料後會自動變色: *** 依據這邊說明的修改這 /static/bulb/ 內的 ida.js    ==> 急性子的同學可先測試 bulb 可從網站叫出再來修改 bulb2 燈泡 :-) (四)現在已經可以把網站跑起來了.. (1)在 CMD 窗啟動隔離環境(不需要時不要重建隔離環境), 假設是 ggyy ggyy\Scripts\activate (2)用 cd 命令進入網站根目錄, 執行 w90.py python w90.py (五)可以先測試你的兩個燈泡了, 開啟瀏覽器叫出首頁 127.0.0.1 或打 Localhost 或打你機器的 IP 也可以 然後點各個連結看看 ! (六)到 demo.iottalk.tw 把 Ball-throw1Ball-throw2 偷回來放入自己網站 (1)準備 VPDA 共用的目錄和檔案, 這次我們希望除了 VPython 的 .py 檔之外全部共用 !!! (a) 在 static 內建立子目錄 vp (等下要放入 index.html) (b) 在 vp 內再建立子目錄 py (這裡要用來放所有的 VPython .py 檔案) (2)瀏覽器開啟 demo.iottalk.tw 準備抓 Ball-throw1 必備的四個檔案 (a) 點 Ball-throw1 開啟, 在空白處按滑鼠右鍵選"檢查" (b) 點 Sources, 點 Pages -- 把 web_py_index.html 存到 /static/vp/index.html -- 把 js/dai.js 存到 /static/vp/dai.js -- 把 js/dan.js 存到 /static/vp/dan.js (c)點 js/dai.js 研究一下, 準備抓 Ball-throw1.py 在瀏覽器網址列: --- 把 web_py_index.html# 改成 py/ --- 在最後補上 .py --- 按下 ENTER 鍵 --- 把 Ball-throw1.py 存到 /static/vp/py/Ball-throw1.py (3)修改剛抓回的四個檔案 (a)修改 /static/vp/index.html 讓它會用到我們的 dan.js 和 dai.js, 其他 .js 都去用 demo.iottalk.tw 的即可! (b)修改 /static/vp/dan.js 找到註冊處, 讓它 d_name 會在左邊多 "XYZ_" 其中 XYZ 是你的姓名的英文頭字語 (c)修改 /static/vp/dai.js -- 讓它會連到正確的 IoTtalk server (大約在 LINE 5 處) -- 讓它會抓到正確的 Audio 聲音檔案, 用 demo.iottalk.tw 即可 (網路共享概念) -- 讓它會抓到正確的 .py 檔案(大約 LINE 132)(da 改為 static 即可) fetch_code('/static/vp/py/'+ project + '.py'); (d)修改用到的 .py 檔案 讓它有聲音 /static/vp/py/Ball-throw1.py 這需要參考 Ball-thjrow2, 所以可以先把 Ball-throw2.py 抓回後再參考修改! (4)讓 Ball-throw2 也可以用 只要參考抓 Ball-throw1.py 方法抓回存入 /static/vp/py/Ball-throw2.py 即可, 都不用修改(除非你想改變 Ball-throw2.py 的行為)! --- 可以參考這 Ball-throw2.py, 修改讓 Ball-throw1 啟動以及球打到地有聲音! --- 可以自定義Good Job, 例如彈兩下內打到 400~500 喊 GoodJob ! (5)抓 Edutalk 上的自由落體來放入我們網站, 改名Free-fall.py(注意大小寫) --- 複製 /static/vp/py/Ball-throw2.py 到 /static/vp/py/Free-fall.py ==> 此時就可測試到首頁點自由落體, 但跑起來是 Ball-throw2 --- 把 Edutalk 上的自由落體實驗的 Program 複製回來, 換掉 Free-fall.py 的內容; 依據 IoTtalk 的規則修改 ! 請參考 0523 加分題的 第(6)項 --- 注意這個檔案要叫做 Free-fall.py 是為了配合前面我們建立的首頁 ! (6)以後只要把 .py 檔案放入 /static/vp/py/ 內, 並修改首頁 index.html 讓它有連結可以點就可多出 VPython DA 了 ! 例如, 目前首頁已經是先預備了給 Hello.py 和 haha.py 用的連結! (七)解決 favicon.ico 的問題, 就是讓你的網頁的頁籤上會顯示 小 icon 因前面 W90.py 檔案已經有寫好這項, 現在建立以下檔案即可: /static/img/myicon.png (當然要先在 static 建立子目錄 img 啦!) ==> 圖片建議用 32 X 32 或 64 X 64 都可以, 用小畫家即可! 好了之後, 到你的首頁 (127.0.0.1) 敲 CTRL_F5 即可 ! 注意看網頁頁籤上的小 icon 有沒變化 !!!   (八)讓你從首頁可以把 VPython 版的 Dummy_Device 叫出來   把以下的程式碼(往下捲一些些)建立成 utf-8 檔案,   放入你的 /static/vp/py/V_Dummy.py 即可 ! (前面 (二) 首頁已做好連結) => 請用 CMD 跑一份 Dummy_Device 來送資料給這 VPython 的 Dummy_device 測試, 最好是 拿 DAI2.py 來改為不轉為 float, 直接把輸入的字串 theInput 送出去即可! 注意, ODF 的上下限必須設 [0, 0] 表示不做 Scale !! (這個 VPython Dummy_Device 已經做了防呆, 不會給輸入資料 "搞死" :-) (九)讓你從首頁可以把 蛇擺 (VPython 版的 Pendulum Wave) 叫出來   (a)要修改首頁 index.html 增加一項 "蛇擺", 指到 #penw (表示要用 penw.py 可自己改名)   (b)把 可搭配 IoTtalk 假裝 Ball-throw1 的蛇擺放入你的 /static/vp/py/penw.py 即可 ! Hint: 只給 Edutalk 版, 要稍微修改(有提示如何改)才可當 IoTtalk 的 VPython DA
 
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
這是 VPython 版本的 Dummy_Device, 只有 ODF, 請設為不 auto Scale 喔!!!
#This 檔案 uses UTF-8  VPython Dummy_Device by tsaiwn@cs.nctu.edu.tw
#  coding=utf-8

#from vpython import *   # 單機版才要 (1), 要當 IoTtalk VPython DA 請註解掉
## 單機版 offline 還要打開後面的 test( ); 會根據 iottalk 是 False 打開
### .. 把以下 iottalk 設 False;  這樣後面也不會做 dai(profile) !
iottalk = True    # using IoTtalk ?  (2)  ==> has sound to play
#scene.autoscale = False   # 看要不要讓場景遠近 AutoScale; 預設是 True
#from time import sleep   # VPython 已經有 sleep;;  Edutalk 也是
#from random import random  # VPython 已經有 random()

runLimit = 3   # 0 表示不會停止
speed = 0.88 #   球的 移動速度 
freq = 60   # 每秒 Loop 幾次 (frame rate)  # 120   # 1000
randSpeedCycle = 2.5 #  2.5 seconds to change Speed 亂數絕定速度
rscCount = int(freq * randSpeedCycle * 0.99)
isControl = False # 還沒參數送進來過 or 沒被重新啟動 by -5566
radius = 0.58
height = radius + 0.1
bbSpeed = speed
t = 0        # 時間
dt = 1.0 / freq
freq10 = int(freq/10)
  
fLong = 33  # 木塊長
fWide = 12  # 木塊寬
fThick = 0.25
isRunning = True
humidity = 58        #  初始濕度, 只是為了 gdots ( )畫圖 
##
## iottalk 要把以下兩個 def 都註解掉; 單機版則要打開

"""     ###   單機版注意 (3) 
#  單機版沒聲音, 因它不認識這兩函數, 幫它定義!
def preloadAudio(name):
    return
def playAudio(name):
    return
"""     ###   單機版注意 (4) 

## Note 注意往下第六列  (inpFst, *_) =  ... 給單機版用 
def getInpFst(inp):      # 故意寫在較前面方便修改切換 IoTtalk 版 <==> VPython 單機版 
    if inp != str(inp):     # 終於想到這招來查看是否輸入字串 or scalar 阿不然一堆函數都不給用!
        return inp    # inp 可能是整數或實數, 不是字串
    if iottalk:  # GlowScript 用 舊版本 Python ?!
        splitStr = inp.split(" ")    #  含有字串, 取最左邊 token 
        inpFst = splitStr[0]
    if not iottalk:  #  單機版用 Python 的 split( ) 
        pass   # 單機版請打開以下 (inpFst, *_) = ... 的; 單機版注意 (5) 是最後要注意項目
        # (inpFst, *_) = inp.split(maxsplit=1);  #  (5) 在 iottalk 還是要註解掉否則會翻譯錯誤!!! 
    return inpFst
#
preloadAudio('Startup.wav')
preloadAudio('chord.wav')
preloadAudio('gj.wav')

def scene_init():
    global scene, ball, bb, floor, gd, xt,vt, hd, c
    global label_info, inp_info, dm_info, chk_info, ch2_info
    scene =  canvas(title="Dummy_Device by tsaiwn@cs.nctu.edu.tw", width=800, height=500, x=0, y=0,
         center = vector(0, 15, 0),    # center=vec(0, 0.1, 0),
         background=vec(0, 0.6, 0.6))
    ball = sphere( pos = vec(0, height, 0), radius = radius, color = color.green,
           velocity = vector(0,0,0), visible = True)
    bb = sphere( pos = vec(0, height, -5), radius = radius, color = vec(255/255,51/255,204/255),
           velocity = vector(0,0,0), visible = True)
    floor = box(pos=vec(0, 0, 0), length=fLong, height=fThick, width=fWide,
        texture=textures.wood)
    gd = graph(title="x-t plot", width=600, height=450,
       x=0, y=988, xtitle="t(s)", ytitle="y Blue=position; pink=speed")
    #gd2 = graph(title="another figure", width=600, height=450, ... 可以另一張圖..
    xt = gcurve(graph=gd, color=color.blue, label='position')
    vt = gcurve(graph=gd, color=vec(255/255,51/255,204/255), label='速度')
    hd = gdots(graph=gd, color=color.green, radius=0.88, label='濕度')
    label_info = label( pos=vec(5,25,-5), text="", color = color.yellow, height=18)
    inp_info = label( pos=vec(5,30,-5), text="", color = color.yellow, height=18)
    dm_info = label( pos=vec(-33,38,-16), text="Dummy_Device", color = color.white, height=24)
    chk_info = label( pos=vec(-28,10,-8), text="chk", color = color.yellow, height=18)
    ch2_info = label( pos=vec(-28,5,-8), text="ch2", color = color.white, height=18)
    c = curve(pos=[vec(-17,10,0), vec(-15,10,0)], color=color.red, radius=0.1)

v1=vec(0,10, 0)  # for gcurve  c.append(v1, v2) 
v2=vec(0,10, 0)
##
def checkbbPos( ):
    global bb
    if (bb.pos.x >= fLong/2 - 0.5* radius):
        bb.pos.x = - fLong/2 + 0.5* radius;
        
def changeSpeed():
    global speed,isControl
    if isControl: return
    speed = speed -0.38 + random( ) * 2   # VPython 已經有 random( ) 
    if(speed > 5):
        speed = 0.38
    
ggyy=0
def setRandSpeed( ):        # 亂數變化速度; 順便改 濕度 
    global speed, ggyy, humidity
    # rate(1,setRandSpeed)   # 只有 Edutalk 內可以這樣 !?
    ggyy += 1
    if ggyy == int(rscCount/2): humidity += int(random( )*20-10) # 隨便亂數變化濕度:-) 
    if ggyy < rscCount:
        return
    ggyy = 0
    changeSpeed( )

def drawCurve( ):
    global v1, v2, speed, c, hd    # 順便 plot 出濕度 hd.plot( )
    v2.x = ball.pos.x
    v2.y = 10+speed   # adjust
    c.append(v1, v2)   #   從 v1 畫到 v2 
    hd.plot( t, humidity);  #  濕度 [0..100] 
    if (v2.x < -5) and (v1.x > 5):   # right to left draw
        c.clear( )
    v1.x = v2.x
    v1.y = v2.y
##
uuyyy = 0
def update_info():
    global uuyyy, label_info, inp_info
    uuyyy += 1
    if uuyyy < freq10: return
    uuyyy = 0
    label_info.text =  "Speed: {:.2f}\nPosition: {:.2f}\nTime: {:.2f}". \
         format(speed, ball.pos.x, t)
    inp_info.text =  "Got data: {} ".format(inp)
    #rate(11, update_info)

def restart( ):
    global saveX
    ball.pos.x =  -(fLong*0.9/2)  # 幾乎最左邊
    bb.pos.x = -(fLong*0.9/2)  # 幾乎最左邊
    saveX = ball.pos.x
    v1.x = ball.pos.x
    v2.x = ball.pos.x
    c.clear( )

def tryCMD(inp):
    global isRunning, howManyRun, saveX
    ans = True
    if inp == "reset" or inp == "restart":
        restart( ); return ans;
    if inp == "pause" or inp == "stop" or inp == "p" or inp == "s":
        saveX = ball.pos.x 
        isRunning = False;
        return ans
    if inp == "go" or inp == "cont" or inp == "continue" or inp == "c":
        if isRunning:
            pass
        else:
            ball.pos.x = saveX
            if howManyRun == 0: howManyRun = runLimit
            isRunning = True
            playAudio('chord.wav')    #  playAudio('go.wav') 
        return ans
    return False
## 當 Dummy_Control 收到新值時
inp = "No Data yet"
saveX = 0
def Dummy_Control(data):
    global inp, isControl, speed, ball, bb, v1, v2, c, saveX
    global chk_info, ch2_info, saveX, isRunning
    if data == None:
        return
    inp = data[0]
    isControl = True
    if tryCMD(inp):  # 處理了命令 go, pause, reset
        return    # 已處理了命令
    inpFst = getInpFst(inp)  # obtain inpFst; 繼續處理 inp 
    chk_info.text = inp     # for Debug
    ch2_info.text = inpFst     # for Debug
    kk = -32768
    try:
        kk = float(inpFst)  # try to get first number 
    except:    # ## 其實我可以在這except 之上, kk = 之下, 下一句放如 gyg = 38 後面檢查gyg
        pass
        return   # 可惡, VPython (Glowscript) 不產生 exception (類似 C語言) 
    # import math   # 還亂改 kk 給我放 NaN; 又沒 math. 也沒 isdigit( ), ord( ) 等
    kkCode = "{:.2f}".format(kk)
    if kkCode == "NaN":    ## float("nan") :  # 終於用這招 kkCode 抓到 NaN :-)
        return
    if kk != -32768:  # has a number  #其實這檢查 -32768 已經沒用了 :-) 
        if kk > 5555:
            isControl = False     # 表示亂數絕定速度 speed
            return
        if kk < -5000:      # 表示 "reset" position
            restart( );
            return
        if kk < -5 : kk = -5
        if kk > 5: kk = 5
        speed = kk
        isRunning = True   # 有改變速度阿就讓它動

tttyyy = 0
def test( ):  #  單機版用來 offline 測試 !
    global tttyyy
    tttyyy = 1 + tttyyy
    if tttyyy == 5*freq:    # 5 秒後
        Dummy_Control(["haha haha hello"])
    if tttyyy == 8*freq:    # 8 秒後 
        Dummy_Control(["3.25 Change SP 改速度 3.25 有看到嗎"])
    if tttyyy == 18*freq:
        Dummy_Control(["2.58 又 Change SP 改速度 2.58"])
    if tttyyy == 21*freq:
        Dummy_Control(["-5555 Now Reset 位置"])
    if tttyyy == 26*freq: 
        Dummy_Control(["5566 Now 恢復亂數控制 speed 速度"])
##
def setup( ):
    scene_init()
    setRandSpeed( );
    profile = {
        'dm_name' : 'Dummy_Device',
        'odf_list' : [ Dummy_Control ],   # df_list  vs. odf_list 
    }
    if iottalk:
        dai(profile)  #  若 單機版, 離線測試沒 dai( ) 可用 
    update_info()
    playAudio('Startup.wav')

setup( )
ball.pos.x = -(fLong*0.9/2)  # 幾乎最左邊
bb.pos.x = -(fLong*0.9/2)  # 幾乎最左邊
#print("ball pos:", ball.pos)
howManyRun = runLimit     # 要跑幾趟 -- 每次到最右邊算一趟
v1.x = ball.pos.x
v2.x = ball.pos.x
v1.y=10
v2.y = 10+speed
# sleep(3)  # 如果單機版想先 delay 一下 
left = - fLong/2 + 0.5* radius;   # 左邊緣
right = fLong/2 - 0.5* radius;     # 右邊緣

while True:
    if not iottalk:  #  offline 測試; 相當於註解掉
        test( )     ##  offline 測試; 相當於註解掉  
    update_info()
    setRandSpeed( );
    rate(freq)
    if not isRunning:
        continue     #  go to while True 
    checkbbPos( );   # 對照球 (粉紅) 位置可能需要調整
    if speed < 0 and (ball.pos.x <= left):  # too Left
        isRunning = False
        speed = 0
        saveX = ball.pos.x
        playAudio('chord.wav')   # 可換別的聲音 
        continue     #  go to while True 
    if speed > 0 and (ball.pos.x >= right):  # 到最右邊了
        howManyRun -= 1      # 每次到最右邊算一趟 
        if howManyRun == 0:
            isRunning = False  # break;   #  若要停止則 break
            saveX = left;  # 稍後會 ..移至最左 Left Side 
            continue     #  go to while True 
        ball.pos.x = left; 
        playAudio('chord.wav')
    ball.pos.x += speed * dt
    bb.pos.x += bbSpeed * dt
    xt.plot(pos = (t, ball.pos.x))
    bvx =  3 * speed   # 放大三倍, 因 Y 軸 scale 被 xt (ball.pos) 掌控了
    vt.plot(pos = (t, bvx))
    drawCurve( )
    t += dt

print("Bye, time = ", t)
sleep(0)  # 讓一下 CPU :-)

* Flask 秘技 ! Flask 秘技 ! Flask 秘技 ! (尼采說:很重要所以要說三次 )
(1)可否讓 .html 網頁內(當然包括在 .js 檔)都省去打 "/static" 呢? 
   Ans: 很簡單, 把你那句  app = Flask(__name__) 那句改為如下即可: 
app = Flask(__name__, static_url_path='/')
=> 意思就是說, 你的 web Root 就是 /static 這子目錄! 請注意, 這時你打如下: 127.0.0.1/abc/index.html 會去抓你的 /static/abc/index.html ==>注意! 這時使用者不可以打 127.0.0.1/static/abc/index.html (根本不知道有 /static 存在 !)
**重要 ! 用了(1)這招會影響 @app.route( ) 使用類似 <path:ggyydir> 這種 pattern 喔
  請參看後面 (4) (5) (6) 說明 !
(2)如果你的"首頁" index.html 根本沒用到 "樣版"(template)的概念, 阿就是說你只是寫: return render_template("index.html") # 根本沒帶參數 那麼, 你也可以考慮把 index.html 放在 /static 子目錄裡面; 這樣, 可以改用以下任一句都可:(選一句; 建議用第一句;;第三句需要 import os 和 send_from_directory )
return app.send_static_file("index.html") # 建議用這個
return send_file("static/index.html") #注意不是 /static/index.html 喔 !
return send_from_directory(os.path.join(app.root_path, 'static'),'index.html')
(3)如前面(1)這樣做還有個好處, 就是你的首頁變有了 "這裡" 可以用! 啥啦!? 就是這時你的 index.html 裡面的 "這裡" (current directory) 就是 /static 裡面 這是因為當你是使用 render_template( ) 時, 你的 current directory "." (這裡) 就是 @app.route( ) 括號內的! 既然你的首頁就是指用 @app.route("/") 的網頁, 你的"這裡"就是 "/"; 本來基於安全理由 "/" 是不能讓使用者存取的 ! 但是, 因為你用了 (1) 那招說: 網址列出現 "主機名稱或IP/" 就是在你的 static 目錄。  反過來說, 你(1)的意思就是 你的 static 子目錄是 web Root (/); 所以說你的 / (web root) 就是 /static (好像繞來讓去, 呵呵!) 聽不懂!? => 因為如果你只是使用原先精簡的 app = Flask(__name__) 則這時的 current directory 是 / 再說一次, 本來基於安全的理由, / 這目錄是不可以用的 !!!    阿這樣意思就是 "這裡" (.) current directory 不存在! "這裡不存在!" ! 白話文就是你的 templates/index.html 內不可以用相對路徑! ==> 但是..但是..但是 如果你用了(1)那招來定義 app, (app 只是一個變數, 你也可改用 ggyy ) 則你的 templates 內的 index.html 檔案就有了 current directory "這裡", 當然, 這時候的"這裡"是你的 /static 不是 /templates 更不是 / 喔!!! (雖然 .html 在 templates 內) ===> 請注意, 上述說明只針對 @app.route("/") 喔 ! ====> 通常, 所謂的"這裡" 是指: (a)如果用 @app.route("指定路徑") => 就是 "指定路徑" 的目錄 (b)不是直接 @app.route("指定路徑") => 就是 .html 檔案所在的目錄
例如  127.0.0.1/abc/gg.html  的 "這裡" 就是 127.0.0.1/abc
如果根本沒 abc 這目錄就會出問題, 想用 /abc/ 內檔案會有問題 !
 (註: 因你可能用 @app.route("/abc/gg.html") 把它 route 到某個存在的 .html )
但如在該 .html 內用 ../js/dai.js 仍可抓到 127.0.0.1/js/dai.js 
(4)可否讓使用者網址中打目錄名稱就自動抓該目錄的 /index.html 呢?
例如, 若使用: 127.0.0.1/bulb 就自動抓 你的 static 目錄之下的 bulb/index.html Ans: 當然可以啊! 也簡單! 記得要 from flask import send_file 只要在你的 web 的 .py 檔案(w90.py)內加入以下route規則: (且你的 app = 要像 (1) 說的那樣定義, 要指定 / 是 static_url_path )
@app.route('/<ggyydir>/')
def giveDirIndex(ggyydir):
   try:    # 不必用 os.path.isfile( ) 檢查, 直接 try 看看 :-)
     return send_file("static/" + ggyydir)  # maybe ggyydir is a File
   except:
     return app.send_static_file(ggyydir + "/index.html") 
但是, 但是, 但是, 這時的 <ggyydir> 只能一層, 就是說不能類似這樣: abc/xxx 所以若打 127.0.0.1/abc/xxx 它不會抓 /static/abc/xxx/index.html => 這是因為搭配 (1)說的 用以下這句定義 app :
app = Flask(__name__, static_url_path='/')
=> 結果會使得手冊說的 <path:ggyydir> 這樣抓不到 "全路徑" !!! 不知道是 Flask 的 Bug 還是故意的就不知道囉 ! 不過, 幸運的是, 這時如果用路徑全名仍可抓到檔案, 例如: 127.0.0.1/abc/xyz/heyyou.html 仍可抓到 /static/abc/xyz/heyyou.html (注意上列路徑沒打也不可打 /static 喔!)
*** 注意喔, 如果用 app = Flask(__name__)  來搭配則抓不到喔 !
   ==> 當然, 這時你只要補上 /static 就可以了, 如:
      127.0.0.1/static/abc/xyz/heyyou.html  
   (廢話, 這是 Flask 基本功能啊 :-) 
  ==> 意思是說只要把檔案丟入 /static 之下或其子子孫孫目錄都可打全路徑抓到!
(5)做了 (4) 這樣後, 會影響我原先 @app.route("/222/") 這種 route 設定嗎 ?
Ans: 不會影響 ! 因為 Flask 很聰明, 它會做出正確的判斷 :-) 不過要記住, 只要你在 @app.route() 之下不是用 return redirect( ), 而是用 return render_template( ) 或 各種 return send_* 的做法, 則你的 .html 檔的 "這裡" 就是 @app.route( ) 括號內路徑 ! 意思就是你的 "相對路徑" 是相對於 @app.route( ) 括號內的路徑啦! (當然包括 .js 檔案, .js 檔的 "這裡" 是依據把它 include 進去的 html 檔案) 還有, 請注意, 如果做了 (1)之後, .html 和 .js 檔案內都不可以再寫 /static 喔 ~ 所以, 以 Vpython Ball-throw1 為例, 這時要檢查: --- .html 是否用到正確的 dan.js 和 dai.js --- dai.js 是否用到正確的 .py 檔案 (6)Q:那有沒辦法多層目錄也會自動抓 index.html 呢? Yes 有! 馬上來寫一個.. (6a) 首先 app 的定義回頭用簡單版: app = Flask(__name__)
from flask import Flask, request
app = Flask(__name__)                       
(6b)記得 import 需要的 module
from flask import render_template, redirect, send_file, send_from_directory
import os
import socket
(6c)把以下規則加入 web 網站的 .py 檔案 (例如 w95.py)
import os  # 多寫一次 import 不會怎樣, 但萬一這和前面也都沒寫會出問題
@app.route('/<path:ggyydir>')   # 注意不可搭配(1)說的指定 static_url_path 
def giveDeepDirIndex(ggyydir):
   #return 'You want path: ' + ggyydir, 202   ##  for debug
   xfile = os.path.join(app.root_path, 'static', ggyydir)
   if os.path.isfile( xfile ):
      return send_file("static/" + ggyydir)  # maybe ggyydir is a File
   elif os.path.isdir( xfile ):   # 目錄先看看是否 "/" 結尾
      if ggyydir.endswith("/"):   ## 是, 改送這目錄的 index.html
         return send_file( "static/" + ggyydir + "/index.html") 
      else:   # 不是 "/" 結尾, 必須轉址, 否則 current directory 不正確 (試好久ㄟ!)
         return redirect( ggyydir + "/" )  ## 轉址後, 會用上面那條規則
   else:
      return ("<h1 style='color:red;'>File %s not found!</h1>" % ggyydir), 404
(6d)你的 127.0.0.1/ 仍可用原先 render_template 方式:
import socket
myhost = socket.gethostname()    ## 我的機器名稱
myip = socket.gethostbyname(myhost)    ## 我的 IP address
@app.route("/")
def webRootNew( ):
   try:
      return render_template('index.html', myname=myhost, myip=myip) 
      # 以下三種是如果你放 static 內(因根本沒用到 Jinja2 template 樣版) 
      # return app.send_static_file("index.html")
      # return send_from_directory(os.path.join(app.root_path, 'static'),'index.html')
      # return send_file("static/index.html")
      pass  # 萬一上面都被註解掉或砍掉 :-)
   except:
      return "<h1 style='color:red;'>No index.html in templates.</h1>", 404 
好啦! 這時 被 render_template 的 首頁 也變有 "這裡" (.) current directory; 因為本來它的 current directory 就是 app.route("/") 括號內的 / 現在因為上面(6c) 的規則, 所有 /file.ext 或 /directory   都變成對應到 /static/file.ext 和 /static/directory 這相當於 首頁的 current directory 就是 /static 這目錄 ! ===> 雖然已經沒使用 app =Flask(__name__, static_url_path='/') 前面說過, 真正的 Web root (/) 基於安全的理由, 是禁止使用的 ! 結論與建議: => 如果你的 .html 根本沒用到 template 樣版, 那就不要放到 templates 目錄去! 就是沒用 Jinja2 樣版的 .html 就放 static 內或 static 各子目錄內; => 送出檔案的方式有很多種, 參考 (2) 和 (4) 說明! => 使用 (1) 的方式定義 app 雖有好處, 也有壞處 : 無法抓到 <path:ggyydir> 的全路徑 !
      還有還有..還有要注意不同寫法會導致 "這裡" (current directory) 不同 !!!
      請注意,  current directory 是 用來寫相對路徑的依據 ! 
==> 再注意!
啥啦!?
就是, 如果你用 @app.route("/222/") 則你的 current directory 就是 /222/ 根據以上寫法, 這時 /222/ 也就是你的 static/222/ 這子目錄 ! (不論你用哪個 send_.. 送出的也不論該 .html 身在何處都這樣喔 !) 除非你用 return redirect( ) 轉址 ! ===> 當然, 若用 return redirect 轉址後, 網址列會出現新的網址, 那個新網址的目錄就是你的 current directory 囉 -
 
## File: par.py  # 簡單 爬蟲範例   by tsaiwn@cs.nctu.edu.tw
# coding=utf-8   ## par.py
import time, datetime
from selenium import webdriver  #從library中引入webdriver
you = webdriver.Chrome()    ## 開啟chrome browser
# Windows 請把 chromeDriver 抓回解壓縮後 和這 .py 檔放同一個目錄下即可 !
you.get('http://www.google.com/');  # try google first
### Mac OS 和 Unix/Linux 須放到 path 路徑內任一個目錄
### 所以如果有起動虛擬隔離環境, 打 echo $PATH 看看有哪些目錄可寫入 ! 
## 簡單說, Mac OS 和 Ububtu 等 Linux/Unix 就放入你虛擬隔離環境的 bin 目錄內啦 ! 
gg = ["https://www.cs.nctu.edu.tw", "https://www.nctu.edu.tw", 
    'https://www.cwb.gov.tw/V8/C/W/OBS_Station.html?ID=46741',
    "https://www.cwb.gov.tw/V8/C/W/OBS_Station.html?ID=46757",
    "https://www.nthu.edu.tw", "https://web.ncku.edu.tw/" ,
    "http://www.mit.edu",  "http://www.edu.tw", "http://www.ibm.com", 
   ]
for k in range(len(gg)):
   try:
      time.sleep(3.5); # 3.5 seconds
      you.get( gg[k] )
      if k < 2:
          time.sleep(2.5); # 交大多停 2.5 秒:-) 
   except:
      pass
time.sleep(5)
you.get(gg[0]) # 跳回交大資工系 :-)
time.sleep(5)
print("Bye ! 再見 :-!")  ## say bye  
you.quit()  #關閉模擬瀏覽器 
# 請把 chromeDriver 抓回解壓縮後和這 .py 檔放同一個目錄下即可
# 如果 .py 執行後, Chrome瀏覽器跳出後又立即閃退, 表示 .. 
#  ..表示  ChromeDriver 和 Chrome瀏覽器 版本不匹配
## .. 點 Chrome 右上角 那點點點 選 說明 選 關於Chrome 仔細看版本
## .. 看快點並立即把該窗關掉, 否則它可能會自動把你更新(目前 83.xxx) 
##  https://chromedriver.storage.googleapis.com/index.html?path=83.0.4103.39/
## 
# 建議最好先建立Python虛擬隔離環境並啟動它 (已建過可啟動舊的即可)
#  python -m venv ggyy
#  ggyy\Scripts\activate   
# 用 pip 安裝所需程式庫, 執行時說缺啥就 pip install 啥!         
#  pip install selenium  # 讓程式可驅動瀏覽器做各種網站操作
#  pip install beautifulsoup4   # 從網頁抓取資料 # pip install bs4  #即可 :-) 
#  pip install lxml   #  XML 和 HTML相關功能; 注意是小寫的 LXML 不是 123 的 1 喔
# how to 安裝 Web Driver:  # 其實只是抓回來解壓縮放好
###  https://sites.google.com/a/chromium.org/chromedriver/
###  請把 chromeDriver.exe 和這 .py 檔放同一個目錄下即可!
## 可以用 CMD 命令窗測試這個 Python 程式碼了! 注意看發生啥事情!
#  python par.py   #  會自動開啟一個瀏覽器 (請預設 Chrome)
## 不要理這信息: Bad attribute EGL Driver message (Error)  ??
參看林云蔚教授提供的氣象局爬蟲範例(在 github) 如果, 如果你執行林云蔚教授提供的爬蟲程式偶爾出現錯誤, 那是因為程式碼故意沒做防呆處理(例外處理, Exception handling) :-) 就是說, 有時候有些時間點, 氣象局沒放代表陰晴天氣的小圖片 阿就找不到 "img" 囉 ! 把有找 "img" 的那列改為如下五列:

    ggyy = tr.find('td',{'headers':'weather'}).find('img')
    if ggyy != None:
        weather.append(ggyy['title'])
    else: 
        weather.append( "啊摘" )  # 不知道啦..晴時多雲偶陣雨吧

或用 try: ... except: ... 也可以 !
比較保險應該每句可能錯的, 都獨立一個 try:... except:... 處理!
Have Fun !

** 如何叫 chromedriver.exe 不要亂秀 Message ?
    阿就 Google: python how to keep webdriver silent (雖然應該是 silently :)
from selenium.webdriver.chrome.options import Options
options = Options()
options.add_argument('--no-sandbox') # Bypass OS security model
options.add_argument("--log-level=3");  
# get rid off Devtool msg / chrome msg ...  
options.add_experimental_option('excludeSwitches', ['enable-logging'])  # 要! 要 !!要 !!!
## do NOT open the browser
options.add_argument("--headless")   # 這樣就不會開啟瀏覽器 :-)
driver = webdriver.Chrome(options=options) 
# for Windows: 把 chromedriver.exe 放要執行 .py 程式的目錄即可 ; 或 PATH 路徑任一目錄也可

再詳細提示如何執行以下這爬蟲程式, 把爬到的資料丟去 IoTtalk 給 Dummy_Device 接收
(1)到 demo.iottalk.tw  點 project 建立一個物聯網應用專案
   引入一個 DM 名稱 Dummy_Device 並把 IDF 和 ODF 都勾選, Save 後,
   畫面出現兩個 Dummy_Device, 左邊只有  IDF (Sensor), 右邊只有 ODF (Dummy_Controol)
(2)點右邊 Dummy_Device 的 Dummy_Control,
   把它下限與上限設為 [0, 0] 表示不做 Auto scale
(3)把左邊 IDF 連線到右邊的 ODF
(4)依據 iottalk.vip/000/ 的使用手冊(四)抓 github 上的 IoTtalk Dummy_device 壓縮檔,
   抓回來解壓縮後放入一個目錄, 共有 DAI.py, DAN,py, csmapi.py 以及一個說明檔
   再依據 iottalk.vip/000/ 的使用手冊(五)把範例存入同一個目錄稱做 DAI2.py
   ==> 這 DAI2.py 會用到 DAN.py 和 csmapi.py
(5)開啟一個 CMD 命令視窗, 執行 python DAI2.py
   ==> 它說缺啥就 pip install 啥
   ====> 執行成功後注意畫面上的註冊成功名稱;
   =====> 建議把這 CMD 窗拉到畫面右邊讓感覺比較對(因 project 畫面有 ODF 的在右邊)!
(6)回 demo.iottalk.tw 的 project 畫面, 點右邊 Dummy_Device 齒輪右邊的長方型,
   把右邊這 Dummy_Device 綁定關聯到剛剛執行的 DAI2.py
(7)把以下程式碼存為 DAI8.py 放入同一個目錄
   ==> 注意檔案一律選擇 utf-8 編碼(encoding)
   ==> 這 DAI8.py 也會用到 DAN.py 和 csmapi.py
(8)開啟另一個 CMD 命令視窗, 執行 python DAI8.py
   => 必要時可以先用 python -m venv yourenv 建立虛擬環境並啟動它!
   ==> 執行時, 它說缺啥就 pip install 啥
   ====> 執行成功後注意畫面上的註冊成功名稱;
   =====> 建議把這 CMD 窗拉到畫面左邊!
(9)回 demo.iottalk.tw 的 project 畫面, 點左邊 Dummy_Device 齒輪右邊的長方型,
   把左邊這 Dummy_Device 綁定關聯到剛剛執行的 DAI8.py
   ==> 必要時點畫面中間上方的 Flush (出現紅燈時要把它點一下變綠燈)

**好了, DAI8.py 已經開始定時每隔約 15 秒去抓氣象局資料並丟去 IoTtalk,
    另外, DAI2.py 會去 IoTtalk 把資料 "拉"(pull)下來並顯示在畫面上!
    還有, 也可在 執行 DAI8.py 的 CMD 視窗打字, 每當按下 ENTER 鍵, 就會被送去 DAI2 的視窗!!
    參考影片 https://youtu.be/7rmDGZItBKY (2小時練習 Web Crawler 讓你增強 Python 功力 20年:-)_善用影片下方資訊!
    後續影片(16分鐘): 改為每個氣象的項目 push 一次, 分不 sleep 與 sleep(0.75): 兩個版本!
 
-- 
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
這是 0526 習題參考答案 ! 要研究才會進步喔!!!
    (但沒照習題規定, 故意把project左右兩邊Dummy_device都弄成綁定關聯到同一個程式即可)
#This 檔案 uses UTF-8 ; coding=utf-8 ;;; by tsaiwn@cs.nctu.edu.tw
# For original Dummy_Device, Google Search github + dummy + iottalk  for other files
import time, DAN, requests, random 
import threading, sys # for using a Thread to read keyboard INPUT

# ServerURL = 'http://Your_server_IP_or_DomainName:9999' #with no secure connection
# ServerURL = 'http://192.168.20.101:9999' #with no secure connection
#  注意你用的 IoTtalk 伺服器網址或 IP  
ServerURL = 'https://demo.iottalk.tw'  # with SSL secure connection
# ServerURL = 'https://Your_DomainName' #with SSL connection  (IP can not be used with https)
Reg_addr = None #if None, Reg_addr = MAC address #(本來在 DAN.py 要這樣做 :-) 
# Note that Reg_addr 在以下三句會被換掉! # the mac_addr in DAN.py is NOT used
mac_addr = 'CD8600D38' + str( random.randint(100,999 ) )  # put here for easy to modify :-) 
# 若希望每次執行這程式都被認為同一個 Dummy_Device, 要把上列 mac_addr 寫死, 不要用亂數。
Reg_addr = mac_addr  # Note that the mac_addr generated in DAN.py always be the same cause using UUID ! 

DAN.profile['dm_name']='Dummy_Device'   # you can change this but should also add the DM in server
DAN.profile['df_list']=['Dummy_Sensor', 'Dummy_Control']   # Check IoTtalk to see what IDF/ODF the DM has
DAN.profile['d_name']= "TWN_D."+ str( random.randint(100,999 ) ) +"_"+ DAN.profile['dm_name'] # None
DAN.device_registration_with_retry(ServerURL, Reg_addr) 
print("dm_name is ", DAN.profile['dm_name']) ; print("Server is ", ServerURL);
# global gotInput, theInput, allDead    ## 主程式不必宣告 globel, 但寫了也 OK
gotInput=False
theInput="haha"
allDead=False

from bs4 import BeautifulSoup
from selenium import webdriver
import datetime

def clearLists( ):
  global date,temp,weather, wind_direction, wind_speed, gust_wind
  global visible, hum, pre, rain, sunlight
  date = []
  temp = []
  weather = []
  wind_direction = []
  wind_speed = []
  gust_wind = []
  visible = []
  hum = []
  pre = []
  rain = []
  sunlight = []

driverReady = False
region = 'hsinchu'
reginTitle="新竹測站觀測資料"

url = 'https://www.cwb.gov.tw/V8/C/W/OBS_Station.html?ID=46757'
from time import sleep   # so that we do NOT have to write time.sleep( ) :-) 
def initCWB(): 
   global  driver, driverReady
   #啟動模擬瀏覽器
   from selenium.webdriver.chrome.options import Options
   options = Options()
   options.add_argument('--no-sandbox') # Bypass OS security model
   options.add_argument("--log-level=3");  # "--disable-logging"  # 尚可, 仍有 Devtool 信息
   ## options.add_argument("--silent")  #  沒用 ! ! !
## get rid off Devtool msg / chrome msg ... 
   options.add_experimental_option('excludeSwitches', ['enable-logging'])  # 要! 要 !!要 !!!
## do NOT open the browser
   options.add_argument("--headless")
##
   try:
      driver = webdriver.Chrome(options=options) # for Windows: 把 chromedriver.exe 放要執行 .py 程式的目錄即可 !
      driverReady = True
   except:
      print("!!! Can NOT get WEB driver !!!")
   #取得網頁代碼
   try:
     driver.get(url)
     open(region+'.html','wb').write(driver.page_source.encode('utf-8'))
   except:
     print("!!! get URL Fail :" + url)

# #對list中的每一項 <tr>
#for tr in trs:
def processTr(tr):
  global date,temp,weather, wind_direction, wind_speed, gust_wind
  global visible, hum, pre, rain, sunlight
  # print("===NOW ", datetime.datetime.now() , flush=True) 
  if tr != None:
    # 使用datetime取得時間年分
    year = str(datetime.datetime.now().year)
#   取時間, <tr>內的<th>, <th>內為時間 月/日<br>時:分
    d = tr.th.text
    d = year + d
#   字串轉為datetime格式
    date.append(datetime.datetime.strptime(d, '%Y%m/%d %H:%M'))
    temp.append(tr.find('td',{'headers':'temp'}).text)
    ggyy = tr.find('td',{'headers':'weather'}).find('img')
    if ggyy != None:
       weather.append(ggyy['title'])
    else: weather.append( "不知道" )
    wind_direction.append(tr.find('td',{'headers':'w-1'}).text)
    wind_speed.append(tr.find('td',{'headers':'w-2'}).text)
    gust_wind.append(tr.find('td',{'headers':'w-3'}).text)
    visible.append(tr.find('td',{'headers':'visible-1'}).text)
    hum.append(tr.find('td',{'headers':'hum'}).text)
    pre.append(tr.find('td',{'headers':'pre'}).text)
    rain.append(tr.find('td',{'headers':'rain'}).text)
    sunlight.append(tr.find('td',{'headers':'sunlight'}).text)

def buildTable( ):
  global table    ## 以下兩列的  global  其實不必要 !
  global date,temp,weather, wind_direction, wind_speed, gust_wind
  global visible, hum, pre, rain, sunlight
  table = {
"觀測時間":date,
"溫度(°C)":temp,
"天氣":weather,
"風向":wind_direction,
"風力 (m/s)":wind_speed,
"陣風 (m/s)":gust_wind,
"能見度(公里)":visible,
"相對溼度(%)":hum,
"海平面氣壓(百帕)":pre,
"當日累積雨量(毫米)":rain,
"日照時數(小時)":sunlight
}

## 這函數會把 line 和 line2 準備好, 分別是次新和最新 的氣象資料 
def prepareData( ):
    global  driver, line, line2, driverReady, table
    #指定 lxml 作為解析器
    soup = BeautifulSoup(driver.page_source, features='lxml')
    sleep(1)  # 其實不必睡一下 :-)
    # <tbody id='obstime'>  # 抓過去24小時資料 (氣象局網頁給的) 
    tbody = soup.find('tbody',{'id':'obstime'})

    # <tbody>内所有<tr>標籤
    trs = tbody.find_all('tr')   ## Google 搜尋 BeautifulSoup + find
    # 把次新的(trs[1]) 串成一列 放 line 
    tr=trs[1]    ## 次新的氣象資料 
    clearLists( )   # 清除所有 11 項 List
    processTr(tr) # 把 tr 資料抓出塞入 11 個 List
    buildTable( ) # 根據 11 個 List 建立 table 這 { dictionary }
    line ="\r\n"
    for gg  in table:     # table 內共有 11 項 
       yy = table[gg][0]   # 每項都是只一個元素的  [List] 
       if gg == "觀測時間":  # 這項特別處理一下
          yy = yy.strftime("%Y-%b-%d %H:%M:%S")
       line += gg +":" + yy
       line += "\r\n"
    #print("line: ", line, flush=True)   
    tr=trs[0]   #  最新的; 因只要 push 兩列, 偷懶沒寫函數, 複製上面一段過來用 :-)
    clearLists( )   # 清除所有 11 項 List   # (主要是後來才想要 push 兩筆資料 :-) 
    processTr(tr)  # 其實直接寫 processTr(trs[0]) 即可
    buildTable( )  # 參看前面處理 trs[1] ..
    line2 ="\r\n"
    for gg in table:    
       yy = table[gg][0]   
       if gg == "觀測時間":   # 特別處理這項
          yy = yy.strftime("%Y-%b-%d %H:%M:%S")
       line2 += gg +":" + yy
       line2 += "\r\n"
    #print("LINE2: ", line2, flush=True)   # for debug  

sendCount = 0 
def grabCWBthenPush( ): 
  global gotInput2, theInput2, allDead, driver, sendCount
  sleep(12);   # give you several time to do binding in your project
  while True:
    initCWB( );
    prepareData( );
    driver.quit()
    # print(line)
    # print(line2)
    while gotInput2:  # 不要跟別人搶 
        time.sleep(0.1)
        continue  # go back to while
    theInput2 = line
    #print("Will send LINE ONE, then sleep a while ==", flush=True)
    gotInput2 = True
    sleep(1);
    while gotInput2:  # 不要跟別人搶
        time.sleep(0.1)
        continue  # go back to while
    theInput2 = line2
    #print("Will send LINE2, then sleep a while ==", flush=True)
    gotInput2 = True  
    sleep(1.25)
    sendCount += 1
    theInput2 = "== ----- End of " + reginTitle +"--" + str(sendCount)
    gotInput2 = True  
    if(allDead) : break
    sleep(15)
    if(allDead) : break

def doRead( ): 
    global gotInput, theInput, allDead
    while True:
        while gotInput:
           time.sleep(0.1)
           continue  # go back to while
        try:
           theInput = input("Give me data: ")
        except Exception:    ##  KeyboardInterrupt:
           allDead = True
           print("\n\nDeregister " + DAN.profile['d_name'] + " !!!\n",  flush=True)
           DAN.deregister()
           sys.stdout = sys.__stdout__
           print(" Thread say Bye bye ---------------", flush=True)
           sys.exit( );   ## break  # raise   #  ?
        gotInput=True
        if theInput =='quit' or theInput == "exit": allDead = True;
        else: print("Will send " + theInput, end="   , ")

#creat a thread to do Input data from keyboard, by tsaiwn@cs.nctu.edu.tw
threadx = threading.Thread(target=doRead)
threadx.daemon = True  # 魔鬼(server) 在主thread結束後也會自殺!
threadx.start()   # 這時 threadx 開始 "同時" 執行
thready = threading.Thread(target=grabCWBthenPush)
thready.daemon = True    # daemon 的 a 不發音 == 魔鬼 == server program
thready.start()   # 這時 thready 也開始 "同時" 執行

line="line first"
line2="line2"     # the following function is used for debug
def check_alive(him):   # check a thread to see if it is alive?
    him.join(timeout=0.0)
    return him.is_alive() 
cnt = 0    ##  for debug 
gotInput = False
gotInput2 = False
while True:     ##  這是 main thread 主執行緒的 典型寫法 : 一個 Loop !
    if(allDead): break;  
    cnt += 1    ## for debug ..
    if cnt % 20 == 0:   # for debug to check thready is alive
       # print("is y-thread alive: ", check_alive(thready), end=", ")
       # print(cnt)
       pass
#  # 以上用來 check 負責抓 CWB 資料的 thread 是否還活著
    try:   ## main thread 依序做以下 (1)(2)(3) 三件事: 
    #(1)Pull data from a device feature called "Dummy_Control" 
        value1=DAN.pull('Dummy_Control')
        if value1 != None:
            print (value1[0])  
    #(2)Push keyboard data to a device feature called "Dummy_Sensor", if any 
        if gotInput:
           if theInput =='quit' or theInput=="exit":
              break;  #  sys.exit( );  
           '''
           #value2=random.uniform(1, 10)
           try:
              value2=float( theInput )
           except:
              value2=0
              ''' 
           if(allDead): break;
           #DAN.push ('Dummy_Sensor', value2,  value2)  #  試這:  
           DAN.push('Dummy_Sensor', theInput)  
           gotInput=False   # so that you can input again 
    #(3)Push CWB data from crawler to a device feature called "Dummy_Sensor", if any 
        if gotInput2:
           DAN.push('Dummy_Sensor', theInput2)
           # DAN.push('Dummy_Sensor', "543 push ggg yyy")   # for debug
           gotInput2=False    ## 通知 thready 爬蟲

    except KeyboardInterrupt:    ## 敲了 CTRL_C
        break;
    except Exception as e:    ## 不明原因, 可能剛剛網路斷了!
        print(e)
        if str(e).find('mac_addr not found:') != -1:
            print('Reg_addr is not found. Try to re-register...')
            DAN.device_registration_with_retry(ServerURL, Reg_addr)
        else:
            print('Connection failed due to unknow reasons.')
            time.sleep(1)    
    try:
       time.sleep(0.2)
    except KeyboardInterrupt:
       break
time.sleep(0.5)
try: 
   DAN.deregister()
except Exception as e:
   print("===")
print("Bye ! --------------", flush=True)
sys.exit( );   ## Quit the program 
* 提醒 IoTtalk 新手入門應用手冊在這: https://iottalk.vip/000/;
   >=== ==> 還沒用過 IoTtalk 物聯網平台請看這 12分鐘影片 關於 IoTtalk 的使用也請先大概看看IoTtalk的使用手冊:
    http://liny.cs.nctu.edu.tw/#IoTtalk(林一平教授網頁) 
  ==> 點入後在 Document 下方, 有中文版和英文版;   也可點這看更多手冊
 * 如想用NodeMCU-ESP8266連接 IoTtalk, 建議也 先看看 ArduTalk 操作手冊 
  


Network Latency 網路延遲 -- 延遲主要受到光速的限制,它在真空中的速度為299,792,458米/秒,這等同於每公里路徑長度需3.33µs。
Network Latency: How to Test, Measure, and Troubleshoot + Best Network Latency Testing Tools of 2020(2019.1007)
2020 Guide to Network Latency – How to Check, Measure, and Reduce Network Latency(2019.0819)
Network Latency Guide & 10 Best Network Latency Test Tools(2019.0301)
What is Network Latency + Using CDNs to Reduce Latency
What is network latency (and how do you use a latency calculator to calculate throughput)?

測試 VM 網路延遲(Microsoft 微軟, 2019.1029)
Azure 網路來回行程延遲統計資料(Microsoft 微軟, 2020.0310)
遊戲觀點 看 網路延遲(Garena 客服)
Faster Low-Latency 5G Mobile Networks(Anritsu, 安立知股份有限公司)

5G通訊將顛覆網路邊緣(2019.03.14 EDN, Taiwan)
What is the Latency of 5G?(2020.02.02, Verizon, USA)
 
台鐵火車查詢
You are the website counter -th visitors.             The W3C Markup Validation Service
* 點這看如何快速安裝 IoTtalk 系統 (要先花錢取得交大授權才可以喔:-)
* 點這看如何選擇 VPS ?(建議用 Hostwinds.com 或流量小可以先用免費的 AWS EC2)

* PuTTY official site to Download PuTTY   PieTTY project official site   Download Pietty0400b14.zip
 
  整理 by 蔡文能 tsaiwn@cs.nctu.edu.tw     交大資工系     tsaiwn.weebly.com     LINE ID: tsaiwn     FB: fb.me/tsaiwn     幾個英文字讀音   TOP
  TOP   Find your FB ID: https://findmyfbid.com/ if you want to Find your Facebook ID for your fb:admins