2017/9/18

Guice


Guice 是 google 推出的一個輕量級依賴注入框架,解決Java中的 Dependency Injection 依賴注入問題,這個功能就像是 Spring 的 DI IoC。但因為 Spring 的框架 scope 龐大,如果只是想要一個單純的 DI library,那麼 Guice 是一個很好的選擇。


JSR 330 DI


在 Spring 誕生後,Google 也提供了另一個 DI 的實作 Guice,後來在 2009 年,定義了 JSR 330 DI 的規範,隨後 Spring 與 Guice 也都支援了 JSR 330。


javax.inject.* 提供了依賴注入的定義類別,但沒有限制依賴配置方式,
依賴配置方式取決於注入器的實作,injector 可以有多種配置設定的方式,可以基於XML、annotation、DSL(Domain-specific language),甚至是Java代碼,在 injector 實作的部分,可以採用反射、代碼生成技術等等,不受限制。




@Inject


可在constructor、field、method上使用,也可以在static 的非 final 的field、method上使用。使用該註解標註的constructor、field、method訪問修飾符 (private、package- private、protected、public 中任意一種) 不受限制。Injector在進行注入時,要按照constructors、fiedls、methods的順序進行。


對被標註 @Inject 的constructor的要求:


  • 在滿足上述說明的情況下,可以有其他的依賴作為方法的參數,別的要求倒沒有什麼。

對被標註 @Inject 的field的要求:


  • 不能是final

對被標註 @Inject 的method的要求:


  • 方法不能是abstract
  • 可以有其他的依賴作為該方法的參數

  • 當一個方法標註了 @Inject 並覆寫了其他標註了 @Inject 的方法時,對於每一個實例的每一次注入請求,該方法只會被注入一次。

  • 當一個方法沒有標註 @Inject 並覆寫了其他標註了 @Inject 的方法時,該方法不會被注入。




@Qualifier


用於標記限定器 annotation,用來指定採用哪個 class


假設 class A 有兩個 subclass A1,A2。B 依賴了A,那麼DI容器在為 B 的實例注入 A 時到底該注入 A1 或 A2 呢?


class B {
    @Inject
    A a;
}

解決方式是在 A1 及 A2 分別寫上 Qualifier 標記


@A_1
public class A1{
}

@A_2
public class A2{
}

在 class B 中指定 A1 或是 A2


class B{
    @Inject
    @A_1
     A a;
}

也可以用 @Named 進行標記


@Named("A1")
Public class A1{
}

@Named("A2")
Public class A2{
}

class B{
    @Inject
    @Named("A1")
    A a;
}



@Scope @Singleton


@Scope 用在 class 上,用來告訴 injector,為該 class 建立多少個 instances。


@Singleton 就是指產生一個 instance。


Guice example in scala sbt project


在 build.sbt 中加上 guice library


libraryDependencies += "com.google.inject" % "guice" % "4.1.0"

定義兩個 Service 介面,分別有 UserServiceImpl 及 LogServiceImpl 實作。


trait UserService {
  def process(): Unit
}

class UserServiceImpl extends UserService {
  override def process(): Unit = {
    System.out.println("UserServiceImpl in process")
  }
}

trait LogService {
  def log(msg: String): Unit
}

class LogServiceImpl extends LogService {

  override def log(msg: String): Unit = {
    System.out.println("log message:" + msg)
  }
}

定義 Application 介面,在 MyApp 實作的 constructor 中,引用了 UserService 及 LogService,將來由 guice 動態指定 UserServiceImpl 及 LogServiceImpl 實作。


import javax.inject.Inject

trait Application {
  def work(): Unit
}

class MyApp @Inject()(val userService: UserService, val logService: LogService) extends Application {
  override def work(): Unit ={
    userService.process()
    logService.log("MyApp is working")
  }
}

Guice 的 Module 定義,必須要 extends AbstractModule,在 configure 中,設定 class 實作的 Denpendency 關係。


import com.google.inject.AbstractModule

class AppModule extends AbstractModule {
  override protected def configure(): Unit = {
    bind(classOf[UserService]).to(classOf[UserServiceImpl])
    bind(classOf[LogService]).to(classOf[LogServiceImpl])

    bind(classOf[Application]).to(classOf[MyApp])
  }
}

scala 主程式,以 Guice.createInjector(new AppModule) 產生 injector,藉由 inject 取得 Application 的 instance,然後就能呼叫 work。


import com.google.inject.Guice

object Main {
  def main(args: Array[String]) {
    println("Main")

    val injector = Guice.createInjector(new AppModule)

    val myApp = injector.getInstance(classOf[Application])

    myApp.work()
  }
}

執行結果


Main
UserServiceImpl in process
log message:MyApp is working



如果要限制 MyApp 為 Singleton,可以在 MyApp 上加上 @Singleton


import javax.inject.{Inject, Singleton}

trait Application {
  def work(): Unit
}

@Singleton
class MyApp @Inject()(val userService: UserService, val logService: LogService) extends Application {
  override def work(): Unit ={
    userService.process()
    logService.log("MyApp is working")
  }
}

也可在 Module 中設定 class dependency 的地方,加上 .in(classOf[Singleton]) 的限制


bind(classOf[Application]).to(classOf[MyApp]).in(classOf[Singleton])

或是寫成 asEagerSingleton,在程式啟動時,就馬上產生 MyApp


bind(classOf[Application]).to(classOf[MyApp]).asEagerSingleton

Spring vs Guice


關於選擇Spring還是Google-Guice的一些想法


SpringComparison


以往的 Spring 是使用 xml 的方式定義 java bean,一般會認為 Guice 處理速度比 Spring 快,但可能只在啟動的時候有差異,因為 spring 需要讀取 xml 設定檔,而 guice 完全都是用程式碼處理的。


Guice 是由 Google 的 AdWords 專案誕生的,他不像是 Spring 整合了許多不同的 Java EE Framework,只是單純且專注在處理 Dependency Injection 的問題。在官方 Spring Comparison 文件中提到一個例子,他是由 Spring 轉換到 Guice,發現大約有 3/4 的程式碼是不需要的,用 Guice 寫的 module 程式碼短,且容易閱讀。


Guice 不支援以設定檔的方式設定 DI,完全是以 annotations 及 generics 程式碼的方式處理,因此可以達成動態 DI 的功能。


References


Guice簡明教程


Guice 快速入門


Guice Getting Started


Google Guice的動機


Java 依賴注入標準(JSR-330)簡介


JSR330 DI

2017/9/11

scala play from 2.5 to 2.6


scala play framework 專案如果要由 2.5 升級到 2.6,必須調整一些設定項目,另外 action composition 部分的程式也有更新的寫法,Lightbend activator 在 2017/5/24 已經退役,現在都要直接使用 sbt 編譯及封裝專案。


build.sbt


scala play 2.6 相關的 library 版本都要更新,scala 也要由 2.11 版改為 2.12,以下是 build.sbt


name := """project"""
organization := "tw.com.maxkit"

version := "0.1.0"

lazy val root = (project in file(".")).enablePlugins(PlayScala, JavaServerAppPackaging)

scalaVersion := "2.12.2"

scalacOptions ++= Seq("-encoding", "UTF-8")

libraryDependencies += guice

// Adds additional packages into Twirl
//TwirlKeys.templateImports += "tw.com.maxkit.controllers._"

// Adds additional packages into conf/routes
// play.sbt.routes.RoutesKeys.routesImport += "tw.com.maxkit.binders._"

libraryDependencies ++= Seq(
  ws,
  filters,
  "com.typesafe.play" %% "play-slick" % "3.0.0",
  "com.typesafe.play" %% "play-slick-evolutions" % "3.0.0",

  // slick
  "com.typesafe.slick" %% "slick" % "3.2.1",
  "org.slf4j" % "slf4j-nop" % "1.6.4",
  "com.typesafe.slick" %% "slick-hikaricp" % "3.2.1",

  // 讓 slick 支援 Timestamp 轉換 slick-joda-mapper https://github.com/tototoshi/slick-joda-mapper
  "com.github.tototoshi" %% "slick-joda-mapper" % "2.3.0",
  "joda-time" % "joda-time" % "2.7",
  "org.joda" % "joda-convert" % "1.7",

  // akka remoteing
  "com.typesafe.akka" % "akka-remote_2.12" % "2.5.3",

  // smtp email plugin
  // https://github.com/playframework/play-mailer
  "com.typesafe.play" %% "play-mailer" % "6.0.0",

  // mariadb java client
  //"mysql" % "mysql-connector-java" % "5.1.36",
  "org.mariadb.jdbc" % "mariadb-java-client" % "2.0.3",

  // 使用 FileUtils
  "commons-io" % "commons-io" % "2.5",

  // 使用 Base64
  "commons-codec" % "commons-codec" % "1.10",

  // object pool
  "commons-pool" % "commons-pool" % "1.6",

  // CLI parser library scopt https://github.com/scopt/scopt
  "com.github.scopt" % "scopt_2.12" % "3.6.0",

  // redis for Play https://github.com/KarelCemus/play-redis
  play.sbt.PlayImport.cacheApi,
  // include play-redis library
  "com.github.karelcemus" %% "play-redis" % "1.5.1",

  // Test Framework
  "org.scalatestplus.play" %% "scalatestplus-play" % "3.1.0" % Test,
  specs2 % Test
)

resolvers ++= Seq(
  "Typesafe repository" at "https://repo.typesafe.com/typesafe/releases/",
  "Typesafe Maven Repository" at "http://repo.typesafe.com/typesafe/maven-releases/",
  "Typesafe ivy" at "http://dl.bintray.com/typesafe/ivy-releases",
  "scalaz-bintray" at "https://dl.bintray.com/scalaz/releases"
)

fork in run := true

// production settings
maintainer := "service <service@maxkit.com.tw>"
packageSummary := "project"
packageDescription := """"""

project/plugins.sbt


resolvers += "Typesafe repository" at "http://repo.typesafe.com/typesafe/releases/"

addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.6.2")

project/build.properties


sbt.version=0.13.15

Action Composition


Actions Composition 2.6 官方文件


如果要在 controller 裡面每一個 method 都進行的登入的驗證,可以用 ActionComposition 的方式實作,但 2.6 版的 ActionComposition 已經調整為以下的做法。


首先獨立實作一個 AuthAction.scala


package controllers.admin

import java.sql.Timestamp
import java.util.Date
import javax.inject.Inject

import model.Usr
import play.api.{Environment, Logger, Mode}
import play.api.cache.redis.CacheApi
import play.api.mvc._
import utils.ServerConstants

import scala.concurrent.{ExecutionContext, Future}

class AuthRequest[A](val usr: Option[Usr], request: Request[A]) extends WrappedRequest[A](request)

class AuthAction @Inject()(cache: CacheApi,
                           env: Environment,
                           val parser: BodyParsers.Default)
                          (implicit val executionContext: ExecutionContext)
  extends ActionBuilder[AuthRequest, AnyContent] with ActionTransformer[Request, AuthRequest] {
  val cacheTimeout = ServerConstants.cacheTimeout

  def transform[A](request: Request[A]) = Future.successful {
    //    val now: Timestamp = new Timestamp(new Date().getTime)
    //    val usr: Usr = new Usr(0, "", "", "", now, "", now, "")
    //
    //    new AuthRequest(Some(usr), request)

    (request.session.get("key").flatMap { key =>
      cache.get[Usr](key)
    } map { usr =>

      // 需要再設定一次 cache,否則會發生 cache timeout
      cache.set(request.session.get("key").get, usr, cacheTimeout)
      new AuthRequest(Some(usr), request)

    }).orElse {

      env.mode match {
        case Mode.Dev | Mode.Test => {
          Logger.info("Mode.Dev don't check admin login status")

          val now: Timestamp = new Timestamp(new Date().getTime)
          val usr: Usr = new Usr(0, "", "devusr", "", now, "", now, "")
          Some( new AuthRequest(Some(usr), request) )
        }
        case Mode.Prod => {
          Some( new AuthRequest(None, request) )
        }
      }

    }.get
  }
}

在 controller 中,要用 injection 的方式將 AuthAction 引用進來。


@Singleton
class MyController @Inject()(
                               authAction: AuthAction,
                               actorSystem: ActorSystem, env: Environment,
                               implicit val executionContext: ExecutionContext,
                               cc: ControllerComponents) extends AbstractController(cc) {
    def listCdrs = authAction.async { request: Request[AnyContent] =>

        val body: AnyContent = request.body
        val formdata: Option[Map[String, Seq[String]]] = body.asFormUrlEncoded
        ........
    }
}

移除 Play.current


在 scala play 2.5 就已經不能用 Play.current,這裡記錄怎麼利用 injector 直接產生 instance。


在一般的 scala class 直接使用 database 的 model,已經不能用以下這種寫法,Play.current、DatabaseConfigProvider.get 都已經是 deprecated method。


val dbConfig = DatabaseConfigProvider.get[JdbcProfile](Play.current)

首先建立一個新的 GlobalContext


package modules

import play.api.inject.Injector
import javax.inject.{Inject,Singleton}

@Singleton
class GlobalContext @Inject()(playInjector: Injector) {
  GlobalContext.injectorRef = playInjector
}

object GlobalContext {
  private var injectorRef: Injector = _

  def injector: Injector = injectorRef
}

在自訂的 Guice Module 中,產生 GlobalContext


bind(classOf[GlobalContext]).asEagerSingleton()

然後就能直接使用 injector 產生 database Model


 val npRepo = GlobalContext.injector.instanceOf[NpRepo]

ref: How to access Play Framework 2.4 guice Injector in application?


Lightbend activator 在 2017/5/24 終止


因為 LIGHTBEND ACTIVATOR TEMPLATES 發布,未來已經不會再用 activator 進行 project template 的管理,要求大家改用 giter8 templates


以往用 activator 產生新的 project 的指令,都要用 sbt 取代。


根據 giter8 template 產生新的 project


sbt new playframework/play-scala-seed.g8

在過程中,要填寫 project name, organization 等資料


This template generates a Play Scala project

name [play-scala-seed]: projectname
organization [com.example]: tw.com.maxkit
scalatestplusplay_version [3.1.1]:
play_version [2.6.2]:

在 poject 中,以往使用 activator 的指令,都要改成 sbt


編譯


sbt compile

啟動 server


sbt run

封裝整個 project


sbt clean update compile stage dist

giter8


Giter8 是一個基於發佈在 Github 或任何 git 上的template來生成文件或目錄的命令行工具,它是以 Scala 實作並由 sbt launcher 運行。


除了一個官方的 giter8 project templates 集散地 之外,我們可以自己建立自己的 project template 並以 git 形式存放及分享在 git server 中。


References


Giter8 gitbook


Giter8


使用Scalatra創建Scala WEB工程


action-composition 2.5

2017/9/4

monorepos


Monorepo 是一種管理企業代碼的方式,在這種方式下會摒棄原先一個 module 一個 git repo 的方式,而是把所有的 modules 都放在一個 repo 內來管理。單體倉庫 monorepos 是一個包含了多個獨立 project 的代碼倉庫,一個代碼 repository 包含一個單體倉庫。


目前有 Babel, React, Angular, Ember, Meteor 等等專案都使用了這個專案管理方式。


multirepos


當一個軟體專案隨著功能跟開發人員的擴增,漸漸地會發生共用程式碼的問題,一個大型的專案會拆分出多個小型 repos,每個 repo 代表了一個單獨的離散想法。


但也因為多個平行專案的發展,會發生一些問題:


  1. 架構孤島: 因為在一個整合專案中,拆分了功能並分配給不同的開發團隊處理及發展,每個團隊在不同的精進道路上,使用了不同的 library,好處是工程師可以根據自由選擇適當的 library,缺點是越來越多的 library,表示開發人員必須花更多時間學習不同的架構。

  2. 依賴地獄(Dependency Hell): 某個程式的修正,會影響到多少專案,在專案整合建構時,會需要很多時間找出整合的問題並提出修正。

  3. 建構耗時: 傳統的循序建構方式,會需要數十分鐘的時間,因應 monorepo 提供的新 build tool,可以平行建構加速建構的過程,也有更快的 incremental build 模式。


build tool


在 monorepo 中會搭配使用一個適當的 build tool,原因當然是為了在這樣的環境下,縮短建構的時間。



沒有萬靈丹


在一個專案建置初期,會因為開發維護的工程師人數不多,傾向於建置一個單一的專案進行開發,隨著應用本身的演進,公司的業務成長,會慢慢增加這個專案開發的參與人員數量,甚至會成長到有一個或多個開發 team。


在多人分工開發的環境下,就會面臨切割專案,抽取共用的函式庫的過程,一般的直覺,就是讓不同的專案模組有各自獨立負責的人員/團隊,也就是有各自獨立的版本演進,最後再整合在一起。


但如果分工是用垂直方式分工,以功能的方式分工,就有可能會造成程式碼的衝突,或是撰寫出來的程式運作的邏輯不同,或是使用了不同的輔助函式庫的問題。


這情況又有 monorepos 這樣的解決方案,想要解決這種問題。


應該說不管什麼樣的方案,都會有相對應的優點及缺點,那就要看每一個使用的情境來決定,該用什麼方式處理,讓優點多一點,缺點少一點,也沒有必要換來換去,畢竟轉換作業方式,加上適應的過程,也需要花上不少時間。


《三國演義》的第一句話就說:「話說天下大勢,分久必合,合久必分。」整體的局勢就是在分分合合中,不斷地來來往往反覆進行。


References


單體代碼倉庫:Uber的Android代碼倉庫演化史


Monorepos in Git


monorepo 新浪潮 | introduce lerna


多包存儲庫管理工具 Lerna


語意化版本 2.0.0

2017/8/28

Firebase Cloud Messaging FCM


GCM 已經被 Firebase Cloud Messaging FCM 取代,以下記錄測試 FCM 的過程。


註冊 Firebase


firebase 的頁面登入 google 帳號,登入後點擊右上方的 "Console" 即可進入控制台,第一件事是建立Firebase 專案,進入主控台後,請按下「CREATE NEW PROJECT」建立一個新專案,專案名稱填 alarm,國家/地區 填台灣。



接下來建立一個 android 應用程式,套件名稱的部分,要跟 Android APP 一樣,我們填 tw.com.maxkit.alarm



建立 Android project


用 Android Studio 建立一個新的 project: Alarm,package name 為 tw.com.maxkit.alarm。


將剛剛新增的 firebase android 應用程式裡面下載的 "google-services.json" 這個檔案,放到 Alarm/app 這個目錄裡面。


修改以下的設定檔


  • Alarm/build.gradle

增加一行 classpath 'com.google.gms:google-services:3.1.0'


buildscript {
    repositories {
        jcenter()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:2.3.2'

        classpath 'com.google.gms:google-services:3.1.0'
    }
}

allprojects {
    repositories {
        jcenter()
    }
}

task clean(type: Delete) {
    delete rootProject.buildDir
}

  • Alarm/app/build.gradle

增加這三個部分


compile 'com.google.firebase:firebase-messaging:10.2.6'
compile 'com.firebase:firebase-jobdispatcher:0.5.2'

apply plugin: 'com.google.gms.google-services'

完整的內容


apply plugin: 'com.android.application'

android {
    compileSdkVersion 24
    buildToolsVersion "25.0.3"
    defaultConfig {
        applicationId "tw.com.maxkit.alarm"
        minSdkVersion 14
        targetSdkVersion 24
        versionCode 1
        versionName "1.0"
        testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })
    compile 'com.android.support:appcompat-v7:24.2.1'
    compile 'com.android.support.constraint:constraint-layout:1.0.2'
    testCompile 'junit:junit:4.12'

    compile 'com.google.firebase:firebase-messaging:10.2.6'
    compile 'com.firebase:firebase-jobdispatcher:0.5.2'
}

apply plugin: 'com.google.gms.google-services'

  • Alarm/app/src/main/AndroidManifest.xml

增加兩個 meta-data,以及 MyFirebaseMessagingService 這個 service


<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="tw.com.maxkit.alarm">

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <!-- 收到通知時, 到狀態列要顯示的 icon -->
        <meta-data
            android:name="com.google.firebase.messaging.default_notification_icon"
            android:resource="@drawable/ic_stat_announcement" />
        <!-- 收到通知的背景色 -->
        <meta-data
            android:name="com.google.firebase.messaging.default_notification_color"
            android:resource="@color/black" />

        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <service
            android:name=".MyFirebaseMessagingService">
            <intent-filter>
                <action android:name="com.google.firebase.MESSAGING_EVENT"/>
            </intent-filter>
        </service>
    </application>

</manifest>

這裡用到了自訂的 color 跟 icon



Alarm/app/src/main/res/values/colors.xml


要增加 black


<color name="black">#00000000</color>

  • MainActivity.java

處理 notification 的部分,由系統列點擊 notification 會進入 MainActivity,在這裡取得該 notification 的資訊。


package tw.com.maxkit.alarm;

import android.content.Intent;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import android.util.Log;

import com.google.firebase.messaging.FirebaseMessaging;

public class MainActivity extends AppCompatActivity {

    private static final String TAG = "MainActivity";

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        //這一行是要註冊 alarms 這個 topic,如果不需要,就把這行刪除
        FirebaseMessaging.getInstance().subscribeToTopic("alarms");

        Log.d(TAG, "onCreate ..");

        Intent intent = getIntent();
        String msg = intent.getStringExtra("msg");

        if (msg!=null)
            Log.d(TAG, "msg:"+msg);

        if (getIntent().getExtras() != null) {
            for (String key : getIntent().getExtras().keySet()) {
                Object value = getIntent().getExtras().get(key);
                Log.d(TAG, "Key: " + key + " Value: " + value);
            }
        }
    }

}

  • MyFirebaseMessagingService.java

處理訊息通知的 code,主要是 onMessageReceived


package tw.com.maxkit.alarm;

import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.media.RingtoneManager;
import android.net.Uri;
import android.support.v4.app.NotificationCompat;
import android.util.Log;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;

public class MyFirebaseMessagingService extends FirebaseMessagingService{

    private static final String TAG = "MyFirebaseMsgService";

    @Override
    public void onMessageReceived(RemoteMessage remoteMessage) {
        Log.d(TAG, "onMessageReceived:"+remoteMessage+", from:"+remoteMessage.getFrom()+", data="+remoteMessage.getData());

        if (remoteMessage.getData().size() > 0) {
            Log.d(TAG, "Message data notifytitle: "+ remoteMessage.getData().get("notifytitle"));
            Log.d(TAG, "Message data notifybody: "+ remoteMessage.getData().get("notifybody"));
            Log.d(TAG, "Message data payload: " + remoteMessage.getData());
        }

        if (remoteMessage.getNotification() != null) {
            Log.d(TAG, "Message Notification title: "+ remoteMessage.getData().get("title"));
            Log.d(TAG, "Message Notification Body: " + remoteMessage.getNotification().getBody());
        }

        //Display notification in notification bar
        sendNotification(remoteMessage.getData().get("notifytitle"), remoteMessage.getData().get("notifybody"));
    }

    private void sendNotification(String notifytitle, String notifybody) {
        Intent intent = new Intent(this, MainActivity.class);

        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
        PendingIntent pendingIntent = PendingIntent.getActivity(this, 0 /* Request code */, intent,
                PendingIntent.FLAG_ONE_SHOT);

        Uri defaultSoundUri=
                RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION);
        NotificationCompat.Builder notificationBuilder = new NotificationCompat.Builder(this)
                .setSmallIcon(R.drawable.ic_stat_access_alarms)
                .setContentTitle( notifytitle )
                .setContentText( notifybody )
                //.setAutoCancel(true)
                .setSound(defaultSoundUri)
                .setContentIntent(pendingIntent);

        NotificationManager notificationManager =
                (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
        notificationManager.notify(0 /* ID of notification */, notificationBuilder.build());
    }
}

用 Firebase 網頁測試


在左側點擊 Notifications 的部分,填寫以下的訊息,就會以 notification 的方式,收到訊息。意思就是說,當 APP 在背景時,會直接在系統列裡面收到 notification。




用 curl 測試


根據 Firebase Cloud Messaging HTTP Protocol 的說明,可以直接用 http 的方式發送訊息。


在專案設定 -> Cloud Messaging 的地方,可以找到 Authorization Key 伺服器金鑰



以下是 curl 的測試,"Authorization: key=" 後面要換成剛剛取得的 伺服器金鑰。


data 的部分可以參考 Firebase Cloud Messaging HTTP Protocol 的說明,以下我們設定了 to alarms 這個 topic,另外只傳送了 data 區塊,在 android APP 不管是前景或背景,都會在 onMessageReceived 收到訊息,我們可在那邊發送 local notification 把資料顯示在系統列上。


curl -X POST -H "Content-Type: application/json" \
    -H "Authorization: key=yourkey" \
    https://fcm.googleapis.com/fcm/send \
    -d '{"to":"/topics/alarms", "priority":"high", "data":{"notifytitle":"測試title", "notifybody":"測試body", "訊息 key": "訊息 Topic!", "key2": "value2"}}'

也可以加上 notification 的區塊,這部分就會跟在 Firebase 頁面測試的結果一樣,Android APP 會直接收到 notification。


curl -X POST -H "Content-Type: application/json" \
    -H "Authorization: key= yourkey" \
    https://fcm.googleapis.com/fcm/send \
    -d '{"to":"/topics/alarms", "priority":"high", "notification":{"title" : "title","icon" : "new","body" : "訊息 body"}, "data":{"訊息 key": "訊息 Topic!", "key2": "value2"}}'

References


Firebase 心得(Cloud Messaging)


在Android中使用2016新版Firebase加快開發過程(一)


Firebase雲端訊息-發送測試通知至Android APP中


Firebase Cloud Messaging (FCM) 入門到進階應用(1) --- 開發環境建立


【Android Studio】從 GCM 移植到 FCM


Android: 利用Firebase實作使用者間的產品分享機制


Send Messages to Multiple Devices


Firebase push Notification using curl command — Devoid Backend




for ios


Push Notification教學:如何使用Firebase在iOS實現推播功能


[教學] 實作Google Firebase的Notification 使用Objective-C

2017/8/21

記錄用 CentOS 7 安裝 Redmine 3.3.3


因機器搬遷,需要把舊的 server 的資料,移到新機器上。


用 docker 測試


docker run -d -p 10022:22 -p 10080:80 centosssh /usr/sbin/sshd -D

如果遇到 Failed to get D-Bus connection: Operation not permitted 的問題,就改用這個方式啟動。因為需要 ssh 及 web 的 port,啟動時先對應好。


ref [原创] 解决 CentOS7 容器 Failed to get D-Bus connection: Operation not permitted


docker run -d -p 10022:22 -p 10080:80 -e "container=docker" --privileged=true -v /sys/fs/cgroup:/sys/fs/cgroup --name centos7test centosssh /usr/sbin/init

docker exec -it centos7test /bin/bash

安裝Apache、MariaDB、PHP


yum install -y httpd php mariadb mariadb-server mariadb-devel systemd which wget

設定MariaDB的DB為utf8


vi /etc/my.cnf.d/server.cnf


[mysqld]
character-set-server=utf8

vi /etc/my.cnf.d/client.cnf


[client]
default-character-set=utf8

啟動 MariaDB


systemctl start mariadb
mysql_secure_installation

建立 redmine 資料庫及帳號


mysql -u root -p

CREATE DATABASE redmine CHARACTER SET utf8;
CREATE USER 'redmine'@'localhost' IDENTIFIED BY 'password';
GRANT ALL PRIVILEGES ON redmine.* TO 'redmine'@'localhost';
flush privileges;
exit;

修改 MariaDB 密碼


mysqladmin -u root password 'password'

安裝 rvm


安裝RVM (Ruby管理工具)、Ruby、Rubygem


# Install Key
gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3
#Install RVM
\curl -sSL https://get.rvm.io | bash -s stable

測試過 ruby 2.4.0 有些問題,所以安裝 ruby 限制在 2.2.4 版。


source /etc/profile.d/rvm.sh
rvm requirements
rvm install ruby 2.2.4
#rvm install rubygem
#gem install rails --no-rdoc --no-ri

不使用 yum 安裝 ruby (現在是 2.0 版)


#yum list ruby 
yum install -y gcc libxml2-devel

# 包含ruby/gem/libyaml
#yum install -y ruby ruby-devel

gem install bundler

gem install rake --no-document
gem i nokogiri --no-document -v='1.6.8'
gem i mime-types --no-document

# ruby 2 與 rails 5 不相容
gem install rails --no-document -v='4.2.7'

gem install rbpdf --no-document
gem install rbpdf-font --no-document

安裝passenger


yum install -y libcurl-devel httpd-devel apr-devel apr-util-devel

gem install passenger
passenger-install-apache2-module

安裝完成後,會出現module passenger的設定檔文字


vi /etc/httpd/conf.d/passenger.conf

LoadModule passenger_module /usr/local/rvm/gems/ruby-2.2.4/gems/passenger-5.1.4/buildout/apache2/mod_passenger.so
<IfModule mod_passenger.c>
     PassengerRoot /usr/local/rvm/gems/ruby-2.2.4/gems/passenger-5.1.4
     PassengerDefaultRuby /usr/local/rvm/gems/ruby-2.2.4/wrappers/ruby
</IfModule>

設定 redmine httpd virtual host


vi /etc/httpd/conf.d/redmine.conf

RailsEnv production
RailsBaseURI /redmine

<Directory /home/redmine/redmine-3.3.3/public>
  Options FollowSymlinks
  AllowOverride none
  Require all granted
</Directory>

restart httpd


systemctl restart httpd

安裝 redmine


cd /home

mkdir redmine
cd redmine

wget http://www.redmine.org/releases/redmine-3.3.3.tar.gz

tar -xf redmine-3.3.3.tar.gz

ln -s /home/redmine/redmine-3.3.3/public /var/www/html/redmine

chown -R apache:apache redmine-3.3.3

設定 mysql


cd redmine-3.3.3/config
cp database.yml.example database.yml

# 設定資料庫使用者名稱、密碼。

vi database.yml

把
production:
  adapter: mysql2
  database: redmine
  host: localhost
  username: root
  password: ""
  encoding: utf8

改為

production:
  adapter: mysql2
  database: redmine
  host: localhost
  username: redmine
  password: "dbpassword"
  encoding: utf8

cd ..

bundle install --without development test rmagick

### 這個是生成redmine的什麼token,不生成的話瀏覽器會連接不上的,每次生成,以前的cookie內容就會失效
bundle exec rake generate_secret_token

如果有舊的 DB 要先 restore redmine db


mysql -uroot -p redmine < redmine_noplugins.sql

升級 DB schema


RAILS_ENV=production bundle exec rake db:migrate

有舊DB 就不需要 loaddefaultdata


# 生成數據庫對象

RAILS_ENV=production REDMINE_LANG=zh bundle exec rake redmine:load_default_data

可用 webrick 內建 web server 測試,也可以跳過不做


bundle exec rails server webrick -e production
wget http://localhost:3000
  > 測試安裝

為以後apache服務器對應(redmine/public目錄)做準備


cd public

cp htaccess.fcgi.example htaccess.fcgi
cp dispatch.fcgi.example dispatch.fcgi

設定 email


cd /home/redmine/redmine-3.3.3/config
cp configuration.yml.example configuration.yml
vi configuration.yml

修改 configuration.yml 前面的 email_delivery,要注意不能修改縮排的格式。


  email_delivery:
    delivery_method: :smtp
    smtp_settings:
      enable_starttls_auto: true
      address: "smtp.gmail.com"
      port: 587
      domain: "maxkit.com.tw"
      authentication: :login
      user_name: "maxkit@maxkit.com.tw"
      password: "youremailpassword"

references


Installing Redmine


CentOS 7 安裝 Redmine


Centos 7安裝 redmine 3.X


最小化安装centos7.3 redmine3.3.3 passenger




Upgrading redmine


Redmine 2.6.3 to 3.0.1 upgrade


redmine_bak to git repository

2017/8/14

以 docker 安裝一個可以遠端 ssh 登入的 centos 7 image


以下紀錄如何產生一個基本的 docker image,安裝了 openssh-server 可以用 ssh 遠端登入。


設定 docker image 以及 openssh-server


docker run -it --name c1 centos:latest /bin/bash

安裝一些基本工具,以及 openssh-server


#yum provides ifconfig

yum install -y net-tools telnet iptables sudo initscripts
yum install -y passwd openssl openssh-server

測試 sshd


/usr/sbin/sshd -D
Could not load host key: /etc/ssh/ssh_host_rsa_key
Could not load host key: /etc/ssh/ssh_host_ecdsa_key
Could not load host key: /etc/ssh/ssh_host_ed25519_key

缺少了一些 key


ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key
#直接 enter 即可

ssh-keygen -t dsa -f /etc/ssh/ssh_host_dsa_key
#直接 enter 即可

ssh-keygen -t ecdsa -f /etc/ssh/ssh_host_ecdsa_key -N ""

ssh-keygen -t ed25519 -f /etc/ssh/ssh_host_ed25519_key -N ""

修改 UsePAM 設定


vi /etc/ssh/sshd_config
# UsePAM yes 改成 UsePAM no
UsePAM no

再測試看看 sshd


/usr/sbin/sshd -D

修改 root 密碼


passwd root

離開 docker


exit

以 docker ps -l 找到剛剛那個 container 的 id


$ docker ps -l
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                     PORTS               NAMES
107fb9c3fc0d        centos:latest       "/bin/bash"         7 minutes ago       Exited (0) 2 seconds ago                       c1

將 container 存成另一個新的 image


docker commit 107fb9c3fc0d centosssh

以新的 image 啟動另一個 docker instance


docker run -d -p 10022:22 centosssh /usr/sbin/sshd -D

現在可以直接 ssh 登入新的 docker machine


ssh root@localhost -p 10022

如果遇到 Failed to get D-Bus connection: Operation not permitted 的問題:ref [原创] 解决 CentOS7 容器 Failed to get D-Bus connection: Operation not permitted


docker run -d -p 10022:22 -e "container=docker" --privileged=true -v /sys/fs/cgroup:/sys/fs/cgroup --name centos7test centosssh /usr/sbin/init

docker exec -it centos7test /bin/bash

gitolite 測試


在新的 docker 機器上安裝 gitolite 測試


yum install -y autoconf git

useradd git
passwd git

產生管理員的 key


ssh-keygen

Generating public/private rsa key pair.
Enter file in which to save the key (/root/.ssh/id_rsa):
Created directory '/root/.ssh'.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in /root/.ssh/id_rsa.
Your public key has been saved in /root/.ssh/id_rsa.pub.
The key fingerprint is:
01:93:46:03:17:6e:e2:06:ec:d6:07:db:2e:13:a3:92 root@1f01b0c5ad69
The key's randomart image is:
+--[ RSA 2048]----+
|    .oBo         |
| .   oo+         |
|  o o.o .        |
| . + *   .       |
|  o B o S        |
| o o =           |
|E . o .          |
| .   o           |
|                 |
+-----------------+

cp /root/.ssh/id_rsa.pub /home/git/admin.pub

以 scp 遠端測試 key


sshpass -p "password" scp -p -P 10022 git@localhost:/home/git/admin.pub .

在本機上安裝 gitolite


su - git

mkdir ~/bin

git clone git://github.com/sitaramc/gitolite

gitolite/install -ln ~/bin

把 admin.pub 放入 gitolite


gitolite setup -pk admin.pub

Initialized empty Git repository in /home/git/repositories/gitolite-admin.git/
Initialized empty Git repository in /home/git/repositories/testing.git/
WARNING: /home/git/.ssh missing; creating a new one
    (this is normal on a brand new install)
WARNING: /home/git/.ssh/authorized_keys missing; creating a new one
    (this is normal on a brand new install)

回到 root 身份


exit

以 git clone gitolite-admin 進行 local git 測試


mkdir test
cd test
git config --global user.email "charley@maxkit.com.tw"
git config --global user.name "charley"

git clone ssh://git@localhost/gitolite-admin

現在就可以利用 gitolite-admin 進行 git 帳號及 repo 維護


放入新的 user key: test.pub 放到 keydir 目錄中


git add keydir/test.pub

修改 conf/gitolite.conf


repo gitolite-admin
    RW+     =   admin
    RW+     =   test

repo testing
    RW+     =   admin
    RW+     =   test

將新的 test 增加到 gitolite-admin 裡面


git add keydir/test.pub
git add conf/gitolite.conf
git commit -m 'add test key'
git push origin master

也可以用遠端的方式存取 git


git clone ssh://git@localhost:10022/gitolite-admin

How to install Gitolite in CentOS 7


Linux 使用 Gitolite 架設 Git Server


使用Gitolite搭建Git服務器


gitolite basic administration


References


centos7中安裝一個可以ssh登陸的docker容器


Docker安裝SSH【Ubuntu、CentOS】

2017/8/7

python tornado websocket server and client


tornado 是一個用Python語言寫成的Web服務器兼Web應用框架,以下記錄如何用 tornado framework 撰寫 websocket Echo Server & Client。


安裝 tornado


在 debian 安裝 python library


wget https://bootstrap.pypa.io/get-pip.py
sudo python get-pip.py

sudo pip install tornado

在 mac 安裝 tornado


sudo port install py27-tornado

Echo Server


# -*- coding: utf-8 -*-

import datetime
import sys
import tornado.httpserver
import tornado.websocket
import tornado.ioloop
import tornado.web

class WSHandler(tornado.websocket.WebSocketHandler):
    clients = []

    def check_origin(self, origin):
        return True

    def open(self):
        print "New client connected"
        #self.write_message("You are connected")
        WSHandler.clients.append(self)

    def on_message(self, message):
        self.write_message(message)

    def on_close(self):
        print "Client disconnected"
        WSHandler.clients.remove(self)

    @classmethod
    def write_to_clients(cls):
        print "Writing to clients"
        for client in cls.clients:
            client.write_message("Hi there!")

application = tornado.web.Application([
    (r"/", WSHandler),
])

if __name__ == "__main__":
    try:
        http_server = tornado.httpserver.HTTPServer(application)
        http_server.listen(9000)
        main_loop = tornado.ioloop.IOLoop.instance()

        # Schedule event (5 seconds from now)
        #main_loop.add_timeout(datetime.timedelta(seconds=5), WSHandler.write_to_clients)

        # background update every x seconds
        # 固定每 5 秒鐘就呼叫一次 WSHandler.write_to_clients 廣播訊息
        task = tornado.ioloop.PeriodicCallback(
                WSHandler.write_to_clients,
                5 * 1000)
        task.start()

        # Start main loop
        #main_loop.start()
        main_loop.make_current()
    except KeyboardInterrupt:
        #print("KeyboardInterrupt")
        sys.exit()

EchoServer in aother Thread


將 Server 放在另一個 Thread 啟動,保留 main thread 用在其他的用途上。


# -*- coding: utf-8 -*-

import datetime
import sys
import tornado.httpserver
import tornado.websocket
import tornado.ioloop
import tornado.web
import os
from threading import Thread

class WSHandler(tornado.websocket.WebSocketHandler):
    clients = []

    def check_origin(self, origin):
        return True

    def open(self):
        print "New client connected"
        #self.write_message("You are connected")
        WSHandler.clients.append(self)

    def on_message(self, message):
        self.write_message(message)

    def on_close(self):
        print "Client disconnected"
        WSHandler.clients.remove(self)

    @classmethod
    def write_to_clients(cls):
        print "Writing to clients"
        for client in cls.clients:
            client.write_message("Hi there!")

class WebThread(Thread):
    def __init__(self):
        Thread.__init__(self, name='WebThread')

    def run(self):
        curdir = os.path.dirname(os.path.realpath(__file__))

        application = tornado.web.Application([
            (r"/", WSHandler),
        ])

        http_server = tornado.httpserver.HTTPServer(application)
        http_server.listen(9000)
        main_loop = tornado.ioloop.IOLoop.instance()

        # Schedule event (5 seconds from now)
        #main_loop.add_timeout(datetime.timedelta(seconds=5), WSHandler.write_to_clients)

        # background update every x seconds
        # 固定每 5 秒鐘就呼叫一次 WSHandler.write_to_clients 廣播訊息
        task = tornado.ioloop.PeriodicCallback(
                WSHandler.write_to_clients,
                5 * 1000)
        task.start()

        main_loop.start()


if __name__ == "__main__":
    try:
        webThread = WebThread()
        webThread.daemon = True
        webThread.start()

        while True:
            pass

    except KeyboardInterrupt:
        #print("KeyboardInterrupt")
        sys.exit()

EchoClient.html



<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.1/jquery.min.js"></script>
<script>

$(document).ready(function(){
    var socket = new WebSocket('ws://127.0.0.1:9000/');

    socket.onopen = function(event){
        socket.send('Hi');
    }

    socket.onmessage = function(event){
        console.log(event.data);
    };

    $(window).unload(function(event){
        socket.close();
    });
});

</script>

Echo Client with tornado framework


#!/usr/bin/env python
# -*- coding: utf-8 -*-

import sys

from tornado.ioloop import IOLoop, PeriodicCallback
from tornado import gen
from tornado.websocket import websocket_connect


class Client(object):
    def __init__(self, url, timeout):
        self.url = url
        self.timeout = timeout
        self.ioloop = IOLoop.instance()
        self.ws = None
        self.connect()

        # 每 20 秒發送一次 ping
        PeriodicCallback(self.keep_alive, 20000, io_loop=self.ioloop).start()

        self.ioloop.start()

    @gen.coroutine
    def connect(self):
        print "trying to connect"
        try:
            self.ws = yield websocket_connect(self.url)
        except Exception, e:
            print "connection error"
        else:
            print "connected"
            self.run()

    @gen.coroutine
    def run(self):
        while True:
            msg = yield self.ws.read_message()
            if msg is None:
                print "connection closed"
                self.ws = None
                break
            else:
                print msg

    def keep_alive(self):
        if self.ws is None:
            self.connect()
        else:
            self.ws.write_message("ping")

if __name__ == "__main__":
    try:
        client = Client("ws://localhost:9000", 5)
    except KeyboardInterrupt:
        #print("KeyboardInterrupt")
        sys.exit()

References


SIMPLE WEB SOCKET CLIENT IMPLEMENTATION USING TORNADO FRAMEWORK.


tornado-websocket-client-example/client.py

2017/7/31

用 socket 將 OpenCV 影像傳送到遠端 client


camera 影像以 socket 傳送到 client 的測試,目前是用 socket,將來還要改成用 websocket 處理,用以接受多個 client 連線的問題。


numpy


numpy 可以快速地將 bytearray 及 martix 進行轉換,透過這邊的程式碼,我們可以了解到,圖片就是一個二維陣列的矩陣,矩陣中每一個點,代表圖片中的一個像素點,而 OpenCV 是以 BGR 的形式儲存像素點的資料。


#!/usr/bin/python
# coding=utf-8

import cv2
import numpy
import os
import time

# 亂數產生 120000 個 bytes, 轉換為 numpy array
randomByteArray = bytearray(os.urandom(120000))
flatNumpyArray = numpy.array(randomByteArray)

# reshape 成 300x400 的矩陣並存成 gray scale 圖片
grayImage = flatNumpyArray.reshape(300,400)
cv2.imwrite('RandomGray.png', grayImage)

# reshape 成 400x100 的矩陣並存成 BGR 圖片
bgrImage = flatNumpyArray.reshape(100,400, 3)
cv2.imwrite('RandomBGR.png', bgrImage)

camera 影像以 socket 傳送到 client


  • 版本1

server.py 等待 client.py 連接,server 接收 client 的 camera 資料


server.py


import socket
import cv2
import numpy

def recvall(sock, count):
    buf = b''
    while count:
        newbuf = sock.recv(count)
        if not newbuf: return None
        buf += newbuf
        count -= len(newbuf)
    return buf

TCP_IP = "192.168.1.152"
TCP_PORT = 8002
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((TCP_IP, TCP_PORT))
s.listen(True)
conn, addr = s.accept()
while 1:
    length = recvall(conn,16)
    stringData = recvall(conn, int(length))
    data = numpy.fromstring(stringData, dtype='uint8')
    decimg=cv2.imdecode(data,1)
    cv2.imshow('SERVER',decimg)
    cv2.waitKey(30)

s.close()
cv2.destroyAllWindows()

client.py


import socket
import cv2
import numpy

TCP_IP = "192.168.1.152"
TCP_PORT = 8002

sock = socket.socket()
capture = cv2.VideoCapture(0)
ret, frame = capture.read()
sock.connect((TCP_IP, TCP_PORT))
encode_param=[int(cv2.IMWRITE_JPEG_QUALITY),90]
while ret:
    result, imgencode = cv2.imencode('.jpg', frame, encode_param)
    data = numpy.array(imgencode)
    stringData = data.tostring()
    sock.send( str(len(stringData)).ljust(16));
    sock.send( stringData );

    ret, frame = capture.read()
    decimg=cv2.imdecode(data,1)
    cv2.imshow('CLIENT',decimg)
    cv2.waitKey(30)

sock.close()
cv2.destroyAllWindows()

  • 版本2

server.py 等待 client.py 連接,client 接收 server 的 camera 資料,顯示在畫面上


server2.py


import socket
import cv2
import numpy

capture = cv2.VideoCapture(0)

TCP_IP = "192.168.1.159"
TCP_PORT = 8002
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.bind((TCP_IP, TCP_PORT))
s.listen(True)

conn, addr = s.accept()

ret, frame = capture.read()
encode_param=[int(cv2.IMWRITE_JPEG_QUALITY),90]

while ret:
    result, imgencode = cv2.imencode('.jpg', frame, encode_param)
    data = numpy.array(imgencode)
    stringData = data.tostring()

    conn.send( str(len(stringData)).ljust(16));
    conn.send( stringData );

    ret, frame = capture.read()
    decimg=cv2.imdecode(data,1)
    cv2.imshow('SERVER2',decimg)
    cv2.waitKey(30)

conn.close()
cv2.destroyAllWindows()

client2.py


import socket
import cv2
import numpy

def recvall(sock, count):
    buf = b''
    while count:
        newbuf = sock.recv(count)
        if not newbuf: return None
        buf += newbuf
        count -= len(newbuf)
    return buf

TCP_IP = "192.168.1.159"
TCP_PORT = 8002

sock = socket.socket()
sock.connect((TCP_IP, TCP_PORT))

while 1:
    length = recvall(sock,16)
    stringData = recvall(sock, int(length))
    data = numpy.fromstring(stringData, dtype='uint8')
    decimg=cv2.imdecode(data,1)
    cv2.imshow('CLIENT2',decimg)
    cv2.waitKey(1)

sock.close()
cv2.destroyAllWindows()

改用 wxPython 作為 GUI


wxWidget 是一個開放原始碼且跨平台的物件工具集(widget toolkit),可用來建立基本的圖形使用者介面,先前測試時,都是以 cv2.imshow 進行畫面 preview,未來為了要製作更複雜的 GUI,所以先測試將畫面改為利用 wxPython 處理。


# mac 上要安裝 py27-wxpython
sudo port install py27-wxpython-3.0

# -*- coding: utf-8 -*-
import wx
import cv2
import time

class TestOpenCV ( wx.Frame ):
    windowWidth = 500
    windowHeight = 320

    def __init__( self, parent=None ):
        wx.Frame.__init__ ( self, parent, id = wx.ID_ANY, title = u"單一視訊畫面", pos = wx.DefaultPosition, size = wx.Size( self.windowWidth, self.windowHeight), style = wx.DEFAULT_FRAME_STYLE|wx.TAB_TRAVERSAL )

        self.SetSizeHintsSz( wx.DefaultSize, wx.DefaultSize )

        bSizer1 = wx.BoxSizer( wx.VERTICAL )

        self.stbmp1 = wx.StaticBitmap( self, wx.ID_ANY, wx.NullBitmap, wx.DefaultPosition, wx.DefaultSize, 0 )
        bSizer1.Add( self.stbmp1, 1, wx.ALL|wx.EXPAND, 5 )
##        self.stbmp1.SetBitmap(wx.Bitmap( u"../image/heats1-f1-02_gray_pressed.png", wx.BITMAP_TYPE_ANY ))

        self.SetSizer( bSizer1 )
        self.Layout()

        self.Centre( wx.BOTH )

    def __del__( self ):
        pass

    def scale_bitmap(self, bitmap, width, height):
        image = wx.ImageFromBitmap(bitmap)
        image = image.Scale(width, height, wx.IMAGE_QUALITY_NORMAL)
        newimg = wx.BitmapFromImage(image)
        return newimg

    def updateImage(self, bitmap):
        # 縮小圖片符合視窗的大小
        newbitmap = self.scale_bitmap(bitmap, self.windowWidth-10, self.windowHeight-30)
        self.stbmp1.SetBitmap(newbitmap)

class App(wx.App):
    """Application class."""

    def OnInit(self):
        self.frame = TestOpenCV()
        self.frame.Show()
        self.SetTopWindow(self.frame)
        self.run()
        return True

    def rot90(self, img, angle):
        if(angle == 270 or angle == -90):
            img = cv2.transpose(img)
            img = cv2.flip(img, 0)  # transpose+flip(0)=CCW
        elif (angle == 180 or angle == -180):
            img = cv2.flip(img, -1)  # transpose+flip(-1)=180
        elif (angle == 90 or angle == -270):
            img = cv2.transpose(img)
            img = cv2.flip(img, 1)  # transpose+flip(1)=CW
        elif (angle == 360 or angle == 0 or angle == -360):
            pass
        else :
            raise Exception("Unknown rotation angle({})".format(angle))
        return img

    def run(self):

        cap = cv2.VideoCapture(0);

        while True:
            ret, frame = cap.read()

            if ret == True:
                # 畫面旋轉 90度
                srcBGR = self.rot90(frame, -90)

                # wxPython 只能處理 RGB 的圖片,要從 BGR 轉 RGB
                srcRGB = cv2.cvtColor(srcBGR, cv2.COLOR_BGR2RGB)

                #print dst.shape  w=720, h=1280
                w, h = srcRGB.shape[:2]

                #dst = cv2.resize(srcRGB, (h/2,w/2), interpolation = cv2.INTER_AREA)
                #wxImage = wx.ImageFromBuffer(h/2, w/2, dst)
                wxImage = wx.ImageFromBuffer(h, w, srcRGB)
                bitmap = wx.BitmapFromImage(wxImage)

                # 更新 視窗上的圖片
                self.frame.updateImage(bitmap)

                #cv2.imshow('frame', dst)
                #if cv2.waitKey(30) & 0xFF == ord('q'):
                #    break

                # sleep 30ms
                time.sleep(0.03)

            else:
                break

        cap.release()
        cv2.destroyAllWindows()


def main():
    app = App()
    app.MainLoop()

if __name__ == '__main__':
    main()

2017/7/24

安裝 OpenCV 3


OpenCV Open Source Computer Vision Library 是一個跨平台的電腦視覺庫,由 Intel 發起並參與開發,由於 Intel 為了推動需要更高速運算的應用,增加硬體的銷售,因此發展了這個機器視覺的運算函式庫,以BSD授權條款授權發行,可以在商業和研究領域中免費使用。OpenCV可用於開發 real time 的圖像處理、電腦視覺以及特徵識別程式。


OpenCV 雖然是以 C++ 寫成,但同時提供了 python 及 java 的 bindings,也因為 python,我們可以用更短的程式碼就完成一些基本的視覺應用,以下紀錄如何在 RPi 3 以及 MacOS 中安裝 OpenCV。


Raspberry Pi 3


RPi 的 camera 是使用 CSI 介面,參考 Raspberry Pi相機模組開箱文 將 RPi 的 camera 裝好。


以 raspi-config 指令 enable camera,然後 reboot。


sudo raspi-config

vcgencmd 是用來查詢一些系統參數的指令,可以用 vcgencmd 測試 camera 狀態。


$ vcgencmd get_camera
supported=1 detected=1

vcgencmd version
vcgencmd get_mem arm
vcgencmd get_mem gpu

安裝 OpenCV 3,必須先更新 RPi,安裝一些基本的工具及 library


sudo apt-get update
sudo apt-get upgrade
sudo apt-get -y install build-essential cmake pkg-config
sudo apt-get -y install cmake-curses-gui htop swig

# load various image file formats from disk
sudo apt-get -y install libjpeg-dev libtiff5-dev libjasper-dev libpng12-dev

#sudo apt-get -y install build-essential cmake cmake-curses-gui pkg-config libpng12-0 libpng12-dev libpng++-dev libpng3 libpnglite-dev zlib1g-dbg zlib1g zlib1g-dev pngtools libtiff5-dev libtiff5 libtiffxx0c2 libtiff-tools libeigen3-dev

#sudo apt-get -y install libjpeg8 libjpeg8-dev libjpeg8-dbg libjpeg-progs swig libv4l-0 libv4l-dev python-numpy

# read various video file formats from disk
sudo apt-get -y install libavcodec-dev libavformat-dev libswscale-dev libv4l-dev
sudo apt-get -y install libxvidcore-dev libx264-dev

# for module: highgui
sudo apt-get -y install libgtk2.0-dev

# matrix operations
sudo apt-get -y install libatlas-base-dev gfortran

# python
sudo apt-get -y install python2.7-dev python3-dev python-numpy python3-numpy

sudo apt-get -y install doxygen

如果需要 tesseract,不安裝也沒關係,可以直接用 pytesseract:


sudo apt-get install -y tesseract-ocr libtesseract-dev libleptonica-dev

安裝 python library


wget https://bootstrap.pypa.io/get-pip.py
sudo python get-pip.py

# for numerical processing
sudo pip install numpy

下載並安裝 OpenCV 3.1


wget -O opencv-3.1.0.zip https://github.com/Itseez/opencv/archive/3.1.0.zip

wget -O opencv_contrib-3.1.0.zip https://github.com/opencv/opencv_contrib/archive/3.1.0.zip

unzip opencv-3.1.0.zip
unzip opencv_contrib-3.1.0.zip

因為 Opencv + Tesseract 編譯會發生問題,所以要把 OpenCV 偵測 Tesseract library 的部分關閉,未來直接用 pytesseract 就可以做 OCR,不用透過 OpenCV 呼叫 tesseract。


#修改 /opt/opencv_contrib-3.1.0/modules/text/FindTesseract.cmake

#增加最後一行
set(Tesseract_FOUND 0)

編譯 opencv


cd opencv-3.1.0/
mkdir build
cd build

# setup build with cmake 或是以 ccmake ../ 用介面設定 build
cmake -D CMAKE_BUILD_TYPE=RELEASE \
    -D CMAKE_INSTALL_PREFIX=/usr/local \
    -D INSTALL_C_EXAMPLES=OFF \
    -D INSTALL_PYTHON_EXAMPLES=ON \
    -D OPENCV_EXTRA_MODULES_PATH=~/opencv/opencv_contrib-3.1.0/modules \
    -D BUILD_EXAMPLES=ON ..

如果不直接用 cmake,也可以用 ccmake gui 設定 cmake 的選項


ccmake ../

# 按 c 產生全新設定,決定細項設定後,修改完後,按 c 設定新的選項,然後再按下 g 即可產生編譯用的設定檔案。

如果 cmake 時會出現這個錯誤


CMake Error at samples/gpu/CMakeLists.txt:100 (list):
  list sub-command REMOVE_ITEM requires list to be present.

參考這裡的解法,要將 INSTALLCEXAMPLES=OFF 設定為 OFF。


# 這個步驟會要做很久,大概是 80 分鐘,如果一直發生問題,就改用 make 去掉 -j4,但會需要 3~4 hrs
# 如果 make -j4 出現 error,可以再用 make -j4 或是改用 make -j2 多試幾次看看,如果錯誤沒有出現在同一個地方,,可以這樣繼續編譯
make -j2

也可以用這樣的方式重複 10 次 make


for i in {1..10}; do make -j2; done

或是用 shell script


#!/bin/bash
for i in {1..10}
do
    make -j2
    if [ $? -ne 0 ]; then
        echo "Try again";
    else
        break;
    fi
done

sudo make install
sudo ldconfig

在 python 的 dist-packages 目錄中,看到 cv2.so 就成功了。


ls -l /usr/local/lib/python2.7/dist-packages/

以 python 測試 cv2


$ python
>>> import cv2
>>> cv2.__version__
'3.1.0'

測試拍照及錄影


raspistill -o t.jpg
raspivid -o t.h264

要讓 OpenCV 使用 camera 必須安裝V4L2套件,在 RPi 必須要先載入 video driver for cv video capture method,camera 的程式才會有作用。


可以直接編譯


cd ~/opencv/
wget http://linuxtv.org/downloads/v4l-utils/v4l-utils-1.6.2.tar.bz2
tar xfvj v4l-utils-1.6.2.tar.bz2

sudo apt-get -y install autoconf gettext libtool libjpeg-dev

cd v4l-utils-1.6.2
autoreconf -vfi
./configure
make
sudo make install

或是用這樣的方式安裝


# 增加sources.list
$ sudo vim /etc/apt/sources.list
於sources.list中寫入以下資訊
deb http://www.linux-projects.org/listing/uv4l_repo/raspbian/ wheezy main

# 加入GPG key
$ sudo wget http://www.linux-projects.org/listing/uv4l_repo/lrkey.asc ~/
$ sudo apt-key add ./lrkey.asc

# 再更新一次系統
$ sudo apt-get update && sudo apt-get upgrade

# 安裝V4L2套件
$ sudo apt-get install uv4l uv4l-raspicam

sudo modprobe bcm2835-v4l2

在modules文件中寫入以下資訊,下次開機就會自動載入這個 module


$ sudo vim /etc/modules
bcm2835-v4l2

測試 /dev/video0


v4l2-ctl --list-ctrls --device /dev/video0

Mac OS El Caption


用類似 RPi 的方式編譯,因為 OpenCV 3.2 會遇到 freetype 編譯錯誤,還不知道怎麼解決,目前只能使用 OpenCV 3.1。另外 python 整合的部分也沒有裝好。所以就不用這樣的方式,改用 macport 直接安裝 opencv +python27。


#安裝 macport, xcode

sudo xcodebuild -license
sudo port install cmake +gui

sudo port install libgphoto2
sudo port install jpeg libpng tiff openexr
sudo port install eigen tbb eigen3
sudo port install py27-numpy

unzip opencv-3.1.0.zip
unzip opencv_contrib-3.1.0.zip

cd opencv-3.1.0
mkdir build
cd build

cmake -D CMAKE_BUILD_TYPE=Release \
    -D CMAKE_INSTALL_PREFIX=/usr/local \
    -D OPENCV_EXTRA_MODULES_PATH=~/project/opencv/opencv_contrib-3.1.0/modules \
    -D BUILD_opencv_python2=ON \
    -D BUILD_opencv_python3=OFF \
    -D INSTALL_PYTHON_EXAMPLES=ON \
    -D INSTALL_C_EXAMPLES=OFF \
    -D BUILD_EXAMPLES=ON \
    -D WITH_EIGEN=OFF \
    -D PYTHON2_PACKAGES_PATH=/opt/local/Library/Frameworks/Python.framework/Versions/2.7/lib/python2.7/site-packages \
    -D PYTHON2_LIBRARY=/opt/local/Library/Frameworks/Python.framework/Versions/2.7/bin \
    -D PYTHON2_INCLUDE_DIR=/opt/local/Library/Frameworks/Python.framework/Versions/2.7/include/python2.7/ \
    ..

make -j4

sudo make install

改用 macport 直接安裝的方式


# 安裝 xcode
xcode-select -install

sudo port selfupdate

sudo port install py27-tkinter
sudo port install cmake +gui
sudo port install py27-scipy

# 設定 python
sudo port install python_select
sudo port slect python python27

# 編譯 opencv
sudo port install opencv +python27 +openni

可以在編譯 opencv 的 Porfile 中查看一些資料,目前是用 3.1.0 版,也已經包含了 opencv_contrib modules,如果真的需要調整編譯的過程,可以設定 macport 的 local repository,複製這個 Portfile,修改編譯的過程。


/opt/local/var/macports/sources/rsync.macports.org/release/tarballs/ports/graphics/opencv/Portfile

測試 1: 打開 1.jpg 顯示在視窗畫面中


test.cpp: 打開 1.jpg 顯示在視窗畫面中


#include <opencv2/opencv.hpp>
using namespace cv;

int main()
{
        Mat img=imread("1.jpg");
        imshow("result",img);
        // 6s後視窗自動關閉
        waitKey(6000);
}

編譯與執行


# 在 mac 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` -stdlib=libc++ test.cpp -o test

# 在 RPi 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` test.cpp -o test

./test

一樣的程式碼,改用 python 寫:test.py


#!/usr/bin/python
# coding=utf-8

import cv2
import time

img = cv2.imread('1.jpg');
cv2.imshow("result",img);
cv2.waitKey()

直接用 python 就可以執行,在 Mac 及 RPi 都一樣。


python test.py

測試 2: 載入圖片, 並用 MORPH_RECT 腐蝕操作


用圖片中暗色的部分,腐蝕高亮的部分。


test2.cpp


#include <opencv2/highgui/highgui.hpp>
#include <opencv2/imgproc/imgproc.hpp>

using namespace cv;

int main(   )
{
    //載入原圖
    Mat srcImage = imread("1.jpg");
    //顯示原圖
    imshow("source", srcImage);
    //進行腐蝕操作
    Mat element = getStructuringElement(MORPH_RECT, Size(15, 15));
    Mat dstImage;
    erode(srcImage, dstImage, element);
    //顯示效果圖
    imshow("result", dstImage);
    waitKey(0);

    return 0;
}

編譯+執行


# 在 mac 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` -stdlib=libc++  test2.cpp -o test2

# 在 RPi 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` test2.cpp -o test2

./test2

test2.py


#!/usr/bin/python
# coding=utf-8

import cv2
import time

# 中文字型
from matplotlib.font_manager import FontProperties
font = FontProperties(fname=r"SimSun.ttc", size=14)

img = cv2.imread('1.jpg');
# grayscale image
#img = cv2.imread('1.jpg', 0);
#img = cv2.imread('1.jpg', cv2.IMREAD_GRAYSCALE)

kernel = cv2.getStructuringElement(cv2.MORPH_RECT,(5,5))
#腐蝕圖像  
eroded = cv2.erode(img,kernel)

#顯示腐蝕後的圖片
cv2.imshow("result", eroded);
cv2.waitKey(0)
cv2.destroyAllWindows()

編譯+執行


python test2.py

測試 3: blur 影像模糊


test3.cpp


#include "opencv2/highgui/highgui.hpp"
#include "opencv2/imgproc/imgproc.hpp"
using namespace cv;

int main( )
{
    Mat srcImage=imread("1.jpg");
    imshow( "source", srcImage );

    Mat dstImage;
    blur( srcImage, dstImage, Size(7, 7));

    imshow( "result" ,dstImage );

    waitKey( 0 );
}

編譯+執行


# 在 mac 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` -stdlib=libc++ test3.cpp -o test3
./test3

# 在 RPi 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` test3.cpp -o test3

./test3

tes3.py


#!/usr/bin/python
# coding=utf-8

import cv2
import time

img = cv2.imread('1.jpg');

blur = cv2.blur(img,(7,7))

cv2.imshow("result", blur);
cv2.waitKey(0)
cv2.destroyAllWindows()

測試 4: canny 邊緣檢測


test4.cpp


#include <opencv2/opencv.hpp>
#include<opencv2/imgproc/imgproc.hpp>
using namespace cv;

int main( )
{
    Mat srcImage = imread("1.jpg");
    imshow("source", srcImage);     //顯示原始圖
    Mat dstImage,edge,grayImage;    //參數定義

    //建立與src同類別型和大小的矩陣(dst)
    dstImage.create( srcImage.size(), srcImage.type() );

    //將原圖像轉換為灰度圖像
    //此句程式碼的OpenCV2版為:
    //cvtColor( srcImage, grayImage, CV_BGR2GRAY );
    //此句程式碼的OpenCV3版為:
    cvtColor( srcImage, grayImage, COLOR_BGR2GRAY );

    // 使用 3x3核心來降噪
    blur( grayImage, edge, Size(3,3) );

    // 執行Canny算子
    Canny( edge, edge, 3, 9,3 );

    imshow("result", edge);

    waitKey(0);

    return 0;
}

編譯+執行


# 在 mac 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` -stdlib=libc++ test4.cpp -o test4
./test4

# 在 RPi 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` test4.cpp -o test4

./test4

test4.py


#!/usr/bin/python
# coding=utf-8

import cv2
import time

# grayscale image
img = cv2.imread('1.jpg', 0);

# 用高斯平滑處理原圖像降噪
img = cv2.GaussianBlur(img,(3,3),0)
# Canny只能處理 grayscale image, 指定最大和最小閾值,其中apertureSize默認為3
canny = cv2.Canny(img, 3, 9)

cv2.imshow("result", canny);
cv2.waitKey(0)
cv2.destroyAllWindows()

test5: 播放 avi 影片


test5.cpp


#include <opencv2/opencv.hpp>
using namespace cv;

//-----------------------------------【main( )函數】--------------------------------------------
//      描述:控制臺應用程式的入口函數,我們的程式從這里開始
//-------------------------------------------------------------------------------------------------
int main( )
{
    VideoCapture capture("1.avi");

    while(1)
    {
        Mat frame;//定義一個Mat變數, 儲存現在的 frame
        capture>>frame;  //讀取現在的 frame
        if (!frame.empty()) {
            imshow("result",frame);
        } else {
            break;
        }
        waitKey(30);  // delay 30ms
    }
    return 0;
}

編譯+執行


# 在 mac 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` -stdlib=libc++ test5.cpp -o test5
./test5

# 在 RPi 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` test5.cpp -o test5

./test5

test5.py


#!/usr/bin/python
# coding=utf-8

import cv2
import time

cap = cv2.VideoCapture('1.avi');

while True:
    ret, frame = cap.read()

    if ret == True:
        ## grayscale avi
        #gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        #cv2.imshow('frame', gray)

        ## normal avi
        cv2.imshow('frame', frame)

        if cv2.waitKey(30) & 0xFF == ord('q'):
            break

    else:
        break

cap.release()
cv2.destroyAllWindows()

test6: 使用 camera


test6.cpp,只有將 VideoCapture capture(0); 換成 camera index 就可以了。


#include <opencv2/opencv.hpp>
using namespace cv;

int main( )
{
    VideoCapture capture(0);
    while(1)
    {
        Mat frame, dst;
        capture>>frame;

        if (!frame.empty()) {
            imshow("result", frame);
        } else {
            break;
        }
        waitKey(30);  //delay 30ms
    }
    return 0;
}

編譯+執行


# 在 mac 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` -stdlib=libc++ test6.cpp -o test6
./test6

# 在 RPi 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` test6.cpp -o test6

./test6

test6.py


#!/usr/bin/python
# coding=utf-8

import cv2
import time

cap = cv2.VideoCapture(0);

while True:
    ret, frame = cap.read()

    if ret == True:
        ## grayscale avi
        #gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        #cv2.imshow('frame', gray)

        ## normal avi
        cv2.imshow('frame', frame)

        if cv2.waitKey(30) & 0xFF == ord('q'):
            break

    else:
        break

cap.release()
cv2.destroyAllWindows()

test7: 使用 camera 加上畫面旋轉


上面的 test6 在 mac 的 camera 角度不對,畫面必須逆時針旋轉一下,才回變成正面。


#include <opencv2/opencv.hpp>
using namespace cv;

void rotate_90n(cv::Mat const &src, cv::Mat &dst, int angle)
{
     CV_Assert(angle % 90 == 0 && angle <= 360 && angle >= -360);
     if(angle == 270 || angle == -90){
        // Rotate clockwise 270 degrees
        cv::transpose(src, dst);
        cv::flip(dst, dst, 0);
    }else if(angle == 180 || angle == -180){
        // Rotate clockwise 180 degrees
        cv::flip(src, dst, -1);
    }else if(angle == 90 || angle == -270){
        // Rotate clockwise 90 degrees
        cv::transpose(src, dst);
        cv::flip(dst, dst, 1);
    }else if(angle == 360 || angle == 0 || angle == -360){
        if(src.data != dst.data){
            src.copyTo(dst);
        }
    }
}

int main( )
{
    VideoCapture capture(0);
    while(1)
    {
        Mat frame, dst;
        capture>>frame;

        if (!frame.empty()) {

            rotate_90n(frame, dst, -90);
            imshow("result", dst);
        } else {
            break;
        }
        waitKey(30);  //delay 30ms
    }
    return 0;
}

編譯+執行


# 在 mac 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` -stdlib=libc++ test7.cpp -o test7
./test7

# 在 RPi 編譯
g++ -ggdb `pkg-config --cflags --libs opencv` test7.cpp -o test7

./test7

test7.py


#!/usr/bin/python
# coding=utf-8

import cv2
import time

def rot90(img, angle):
    if(angle == 270 or angle == -90):
        img = cv2.transpose(img)
        img = cv2.flip(img, 0)  # transpose+flip(0)=CCW
    elif (angle == 180 or angle == -180):
        img = cv2.flip(img, -1)  # transpose+flip(-1)=180
    elif (angle == 90 or angle == -270):
        img = cv2.transpose(img)
        img = cv2.flip(img, 1)  # transpose+flip(1)=CW
    elif (angle == 360 or angle == 0 or angle == -360):
        pass
    else :
        raise Exception("Unknown rotation angle({})".format(angle))
    return img

cap = cv2.VideoCapture(0);

while True:
    ret, frame = cap.read()

    if ret == True:
        ## grayscale avi
        #gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
        #cv2.imshow('frame', gray)

        ## normal avi
        dst = rot90(frame, -90)
        cv2.imshow('frame', dst)

        if cv2.waitKey(30) & 0xFF == ord('q'):
            break

    else:
        break

cap.release()
cv2.destroyAllWindows()

References


Install OpenCV-Python in Windows


Computer Vision with Raspberry Pi and the Camera Pi module


使用 vcgencmd 指令查看 Raspberry Pi 的 CPU 溫度、運行速度與電壓等資訊


在 Raspberry Pi 上面安裝 OpenCV 函式庫


安裝 OPENCV 紀錄


Install guide: Raspberry Pi 3 + Raspbian Jessie + OpenCV 3


[翻译]Python 2.7 和 Python 3+ 的OpenCV 3.0 安装教程


[Raspberry Pi] 解決 Raspberry Pi 找不到 /dev/video0


OpenCV on Raspberry Pi - Using Java(6)- 使用 OpenCV 拍攝照片(Camera Module)




Installing OpenCV in Mac OSx tutorial


macOS: Install OpenCV 3 and Python 2.7


Mac下安装OpenCV3.0—包含opencv_contrib模块


Undefined freetype symbols when building openCV 3.2.0




How to compile OpenCV sample code ?


【OpenCV】安裝在Mac及XCode筆記


VideoCapture.open(0) won't recognize pi cam


Rotate image by 90, 180 or 270 degrees

2017/7/17

使用 Tsung 測試 websocket


tsung是erlang開發的多協議分佈式負載測試工具,它能用來壓力測試HTTP, WebDAV, SOAP, PostgreSQL, MySQL, LDAP, webscoket 和 Jabber/XMPP 的 server。


安裝 Tsung


在 mac 安裝 tsung 可使用 macport


sudo port install tsung

安裝成功後,以 -v 測試


$ tsung -v
Tsung version 1.6.0

在這個路徑下面,有一些 perl script,接下來可以利用這些 script 產生報表


$ $ ls -m /opt/local/lib/tsung/bin/
log2tsung.pl, tsung-rrd.pl, tsung_percentile.pl, tsung_stats.pl

範例的路徑


/opt/local/share/doc/tsung/examples

我們可以將 websocket.xml 範例複製出來修改。


cp /opt/local/share/doc/tsung/examples/websocket.xml .

撰寫 test scenario


修改從範例複製的 websocket.xml


<?xml version="1.0"?>
<!DOCTYPE tsung SYSTEM "/opt/local/share/tsung/tsung-1.0.dtd" []>
<tsung loglevel="notice" version="1.0">
  <clients>
    <client host="localhost" use_controller_vm="true" maxusers="1000" />
  </clients>

  <servers>
    <server host="127.0.0.1" port="9000" type="tcp" />
  </servers>

  <load>
    <!-- phase 1: 0.1 usr per second in 10 minutes, max: 1000 -->
    <arrivalphase phase="1" duration="10" unit="minute">
      <users maxnumber="1000" arrivalrate="0.1" unit="second" />
    </arrivalphase>

    <!-- phase 2: 1 new user every second in 10 minutes -->
    <!--
    <arrivalphase phase="2" duration="10" unit="minute">
      <users interarrival="1" unit="second"></users>
    </arrivalphase>
    -->

    <!-- phase 3: 1 new usr every 0.1 second in 10 minutes -->
    <!--
    <arrivalphase phase="3" duration="10" unit="minute">
      <users interarrival="0.1" unit="second"></users>
    </arrivalphase>
    -->

    <!-- phase 4: 10 usrs per second in 10 minutes, the same as phase 3, max: 100 -->
    <!--
    <arrivalphase phase="4" duration="10" unit="minute">
      <users maxnumber="100" arrivalrate="10" unit="second"></users>
    </arrivalphase>
    -->
  </load>

  <options>
    <option name="file_server" id='userdb' value="usr.csv"/>
  </options>

  <sessions>
    <session name="websocket" probability="100" type="ts_websocket">
        <setdynvars sourcetype="file" fileid="userdb" delimiter=";" order="iter">
          <var name="usrid"/>
          <var name="pwd"/>
        </setdynvars>

        <request subst="true">
             <websocket type="connect" path="/msgsrv/ws"></websocket>
        </request>

        <thinktime min="2" max="5" random="true"></thinktime>

        <request subst="true">

            <websocket type="message">
{
  "task":1,
  "action":1,
  "serial":"01-01-%%_usrid%%",
  "rdata":{
    "userid":"%%_usrid%%",
    "userpwd":"%%_pwd%%"
  }
}
            </websocket>
        </request>

        <thinktime min="3" max="10" random="true"></thinktime>

        <request>
            <websocket type="message" >
{
  "task":3,
  "action":2,
  "serial":"03-02-%%_usrid%%",
  "rdata":{
    "areaseq":1
  }
}
            </websocket>
        </request>

        <thinktime value="30"></thinktime>

        <request>
            <websocket type="close"></websocket>
        </request>
    </session>
  </sessions>
</tsung>

以下說明每個部分的設定:


load 的部分是新連線產生的速度,下面有四個 phases,分別有不同的連線產生的速度


<load>
    <!-- phase 1: 0.1 usr per second in 10 minutes, max: 1000 -->
    <arrivalphase phase="1" duration="10" unit="minute">
      <users maxnumber="1000" arrivalrate="0.1" unit="second" />
    </arrivalphase>

    <!-- phase 2: 1 new user every second in 10 minutes -->
    <arrivalphase phase="2" duration="10" unit="minute">
      <users interarrival="1" unit="second"></users>
    </arrivalphase>

    <!-- phase 3: 1 new usr every 0.1 second in 10 minutes -->
    <arrivalphase phase="3" duration="10" unit="minute">
      <users interarrival="0.1" unit="second"></users>
    </arrivalphase>

    <!-- phase 4: 10 usrs per second in 10 minutes, the same as phase 3, max: 100 -->
    <arrivalphase phase="4" duration="10" unit="minute">
      <users maxnumber="100" arrivalrate="10" unit="second"></users>
    </arrivalphase>
  </load>

option 是定義一個外部 csv 檔案的 id,給後面參考使用


  <options>
    <option name="file_server" id='userdb' value="usr.csv"/>
  </options>

session 的一開始,先取得 userdb 對應的 csv 檔案,並取得兩欄資料,對應到 usrid 及 pwd 兩個變數。後面在 request 的地方,可以用 %%_usrid%% 參考到 csv 裡面的變數。


<session name="websocket" probability="100" type="ts_websocket">
        <setdynvars sourcetype="file" fileid="userdb" delimiter=";" order="iter">
          <var name="usrid"/>
          <var name="pwd"/>
        </setdynvars>

        <request subst="true">
             <websocket type="connect" path="/msgsrv/ws"></websocket>
        </request>

        <thinktime min="2" max="5" random="true"></thinktime>

        <request subst="true">

            <websocket type="message">
{
  "task":1,
  "action":1,
  "serial":"01-01-%%_usrid%%",
  "rdata":{
    "userid":"%%_usrid%%",
    "userpwd":"%%_pwd%%"
  }
}
            </websocket>
        </request>

        <thinktime min="3" max="10" random="true"></thinktime>
</session>

usr.csv 裡面就放兩欄,分別是 usrid 及 pwd


u0001;u0001pwd
u0002;u0002pwd
u0003;u0003pwd

進行 tsung 測試


啟動 tsung


tsung -f websocket.xml -l log start

產生的報告會放在 log 目錄下面,一個日期 timestamp的目錄中


tail -f log/20170324-1531/tsung.log

可以用 tsung_stats.pl 產生 html 的 report


mkdir report

cd report

/opt/local/lib/tsung/bin/tsung_stats.pl --stats ../log/20170324-1531/tsung.log chromium graph.html

最後可以看到一些統計報表



References


Install tsung centos 7


installing tsung in centos


CentOS下安裝 Tsung-壓力測試工具


CentOS壓力測試工具Tsung安裝和圖形報表生成


詳解CentOS下Tsung環境的搭建到跑通第一個測試用例


Load Testing using Tsung


tsung的使用筆記


Tsung’s documentation


tsung測試openfire時從CSV文件讀取user信息

2017/7/3

使用 vim Plugins 改造 vim 為開發的 IDE


在使用 linux 一定知道要用 vim,vim 提供了很多 plugins 讓我們可以調整 vim,而一個大型的 programming IDE,核心也是從文字編輯器開始,延伸其他的功能,合併成一個完整的 IDE。


這麼多 plugins,如果要瞭解內容跟細節,還是一個一個慢慢安裝,閱讀文件才會比較清楚該 plugin 能做什麼事情,不過也要花時間慢慢去調整出適合自己的工作環境。


利用 vundle 快速設定 vim


ref: Configuring Vim as an IDE


Vundle是vim plugin的管理工具,利用 vundle 可以快速將很多 plugins 一次安裝起來。


首先將以下文件內容寫入 ~/.vimrc


syntax on

set nocompatible
set smartindent
set shiftwidth=4
set backspace=indent,eol,start
set ruler
set number
set showcmd
set incsearch
set hlsearch
set hls
set ic
set pastetoggle=<F12>
set enc=utf8

set mouse=a

filetype off 

" set the runtime path to include Vundle and initialize
set rtp+=~/.vim/bundle/Vundle.vim



"-------------- PLUGINS STARTS -----------------
call vundle#begin()

Plugin 'VundleVim/Vundle.vim'
Plugin 'vim-airline/vim-airline'
Plugin 'vim-airline/vim-airline-themes'
Plugin 'altercation/vim-colors-solarized'
Plugin 'scrooloose/nerdtree'
Plugin 'jistr/vim-nerdtree-tabs'
Plugin 'scrooloose/syntastic'
Plugin 'xolox/vim-misc'
Plugin 'xolox/vim-easytags'
Plugin 'majutsushi/tagbar'
Plugin 'ctrlpvim/ctrlp.vim'
Plugin 'vim-scripts/a.vim'
Plugin 'airblade/vim-gitgutter'
Plugin 'tpope/vim-fugitive'
Plugin 'Raimondi/delimitMate'
Plugin 'christoomey/vim-tmux-navigator'
Plugin 'jez/vim-c0'
Plugin 'jez/vim-ispc'
Plugin 'kchmck/vim-coffee-script'
Plugin 'flazz/vim-colorschemes'

call vundle#end()  
"-------------- PLUGINS END --------------------
filetype plugin indent on



"----- GENERAL SETTINGS-------
set laststatus=2
let g:airline_powerline_fonts = 1
let g:airline_detect_paste=1
let g:airline#extensions#tabline#enabled = 1
let g:airline_theme='solarized'
set background=dark
let g:solarized_termcolors=256
colorscheme solarized


"---------NERD-TREE SETTINGS----------
nmap <silent> <leader>t :NERDTreeTabsToggle<CR>
let g:nerdtree_tabs_open_on_console_startup = 1


"-------- SYNTASTIC SETTINGS---------
let g:syntastic_error_symbol = '✘'
let g:syntastic_warning_symbol = "▲"

augroup mySyntastic
    au!
    au FileType tex let b:syntastic_mode = "passive"
augroup END


"-------- TAGS SETTINGS --------------------------------
let g:easytags_events = ['BufReadPost', 'BufWritePost']
let g:easytags_async = 1
let g:easytags_dynamic_files = 2
let g:easytags_resolve_links = 1
let g:easytags_suppress_ctags_warning = 1
let g:tagbar_autoclose=2

nmap <silent> <leader>b :TagbarToggle<CR>
"autocmd BufEnter * nested :call tagbar#autoopen(0)

"---------GIT SETTINGS--------------
hi clear SignColumn
let g:airline#extensions#hunks#non_zero_only = 1


"----------DELIMITEMATE SETTINGS-----------------
let delimitMate_expand_cr = 1
augroup mydelimitMate
    au!
    au FileType markdown let b:delimitMate_nesting_quotes = ["`"]
    au FileType tex let b:delimitMate_quotes = ""
    au FileType tex let b:delimitMate_matchpairs = "(:),[:],{:},`:'"
    au FileType python let b:delimitMate_nesting_quotes = ['"', "'"]
augroup END

"-----------TMUX SETTINGS--------------
let g:tmux_navigator_save_on_switch = 2

以 git 安裝 vundle


git clone https://github.com/VundleVim/Vundle.vim.git ~/.vim/bundle/Vundle.vim

在 CentOS 要安裝 ctags


yum install ctags

如果是 Debian 則是


sudo apt-get install exuberant-ctags

安裝字型 Menlo-for-Powerline


git clone https://github.com/abertsch/Menlo-for-Powerline.git

mkdir ~/.fonts
cd Menlo-for-Powerline/
cp "Menlo for Powerline.ttf" ~/.fonts/
fc-cache -vf ~/.fonts

安裝 color solarized


cd ~/.vim/bundle
git clone git://github.com/altercation/vim-colors-solarized.git

最後在 console 執行,花一點時間,就可以安裝 .vimrc 指定的所有 plugins


vim +PluginInstall +qall

透過以上方式,就安裝了


  1. Vundle
  2. vim-airline
  3. vim-airline-themes
  4. vim-colors-solarized
  5. nerdtree
  6. vim-nerdtree-tabs
  7. syntastic
  8. vim-misc
  9. vim-easytags
  10. ctrlp.vim
  11. a.vim
  12. vim-gitgutter
  13. vim-fugitive
  14. delimitMate
  15. vim-tmux-navigator
  16. vim-colorschemes

NERDTree


ref: 上古神器vim插件:你真的學會用NERDTree了嗎?


NERDTree 是安裝後進入 vim,馬上就會看到的介面,他是一個檔案管理員的功能。


使用 NERDTree 搭配 nerdtree-tabs,感覺就比較接近一般的 IDE。


NERDTree 的指令有這些,但其實只要用滑鼠,點擊目錄跟檔案,也可以打開檔案


?: 快速幫助文檔
o: 打開一個目錄或者打開文件,創建的是buffer,也可以用來打開書籤
go: 打開一個文件,但是光標仍然留在NERDTree,創建的是buffer
t: 打開一個文件,創建的是Tab,對書籤同樣生效
T: 打開一個文件,但是光標仍然留在NERDTree,創建的是Tab,對書籤同樣生效
i: 水平分割創建文件的窗口,創建的是buffer
gi: 水平分割創建文件的窗口,但是光標仍然留在NERDTree
s: 垂直分割創建文件的窗口,創建的是buffer
gs: 和gi,go類似
x: 收起當前打開的目錄
X: 收起所有打開的目錄
e: 以文件管理的方式打開選中的目錄
D: 刪除書籤
P: 大寫,跳轉到當前根路徑
p: 小寫,跳轉到光標所在的上一級路徑
K: 跳轉到第一個子路徑
J: 跳轉到最後一個子路徑
<C-j>和<C-k>: 在同級目錄和文件間移動,忽略子目錄和子文件
C: 將根路徑設置為光標所在的目錄
u: 設置上級目錄為根路徑
U: 設置上級目錄為跟路徑,但是維持原來目錄打開的狀態
r: 刷新光標所在的目錄
R: 刷新當前根路徑
I: 顯示或者不顯示隱藏文件
f: 打開和關閉文件過濾器
q: 關閉NERDTree
A: 全屏顯示NERDTree,或者關閉全屏

Java, Scala, Python...


以 java 開發為例,最基本就是要在編輯 .java 檔案時,填寫 System. 的時候,就要自動出現該 package 可以填寫的 package or class。


在 .vimrc plugins 區塊,call vundle#end() 的前面加上


" java import
Plugin 'javaimp.vim'
" java auto complete
Plugin 'javacomplete'

.vimrc 最後面加上 javacomplete 的設定


" ========== omnifunc:javacomplete 自動補全功能 =========
" 設定此行在 java 檔案中,就可按(ctrl + x) + (ctrl + o) 自動補全
setlocal omnifunc=javacomplete#Complete
" 當檔案為副檔名為 java 動作
" mode的狀態下,按"."會替換成以下指令,換言之,與ide相同當按"."會自動補全
autocmd Filetype java,jsp set omnifunc=javacomplete#Complete
autocmd Filetype java,jsp set completefunc=javacomplete#CompleteParamsInf
autocmd Filetype java,jsp inoremap <buffer> . .<C-X><C-O><C-P><DOWN>
" 設定額外 include 的 classpath
" let b:classpath="/opt/apache-tomcat-8.0.30/lib/*"

" ========== omnifunc:javacomplete2 自動補全功能 =========
autocmd FileType java setlocal omnifunc=javacomplete#Complete
let g:JavaComplete_MavenRepositoryDisable = 1
let g:JavaComplete_UseFQN = 1
let g:JavaComplete_ClosingBrace = 1
let g:JavaComplete_JavaviDebug = 1
let g:JavaComplete_ImportDefault = 0

再安裝一次


vim +PluginInstall +qall

編輯 .java 就可以出現這樣的功能



References


Scala development in Vim


Coding Scala with Vim


vim plugin 推薦 (For python and java )


用 Vim 寫 JAVA - 環境建立 與 Eclim


Coding Java with Vim, 打造自己的工作環境


vim 使用 javacomplete2 自動補齊功能,只有 root 權限能完全工作問題


vim plugin 推薦 (For python and java )