Pebble Timeで天気を表示するWatch Faceを作ってみた(Pip-boyを添えて)

あむちょです。

そろそろネタがきれそうですが、またWatch Faceを公開

PR アプリ作ってます
ad maruta

目次

Pebble Timeってなんですかって人はこちら。丸型スマートウォッチPebble Time Roundのレビュー

今回作ったのは、位置情報から天気予報APIを使って天気予報を表示してみました。

pip boyrect pip boyround

FalloutってゲームのPip boyってキャラクターが無駄に歩きます。

storerect1

充電中は別のアニメーションに切り替わります。

スマホの位置情報がOnの時は、現在地の天気予報を取得し、Offの時は設定画面で入力した都市の天気予報を表示します。

こちらからダウンロードできます。Pebble Time Watch Face 「PiP-Boy」

ソースコード

pebble.jsで位置情報、もしくは都市を天気予報APIに渡し、Jsonでデータを取得してPebble本体に送信してます。

まずはCファイル

#include 

#define IMG_MAXNUM 8
#define IMG_MAXNUM_GOOD 6
#define EMPTY_SLOT -1

//#define DEBAG

static Window *window;
static BitmapLayer *image_layer;
static BitmapLayer *imageWeather_layer;
static GBitmap *image_walk[IMG_MAXNUM];
static GBitmap *image_good[IMG_MAXNUM_GOOD];
static GBitmap *image_weather;

static TextLayer *textTime_layer;
static TextLayer *textYear_layer;
static TextLayer *textBt_layer;
static TextLayer *textTMP_layer;
static TextLayer *textCTY_layer;
static TextLayer *textHUM_layer;

static int cnt = 0;
static int image_state = EMPTY_SLOT;
static int image_state_good = EMPTY_SLOT;
static int image_state_weather = EMPTY_SLOT;
static AppTimer *timer;
static bool charge = false;
static const int RESOURCE_IDS[] = {
    RESOURCE_ID_IMAGE0,RESOURCE_ID_IMAGE1,RESOURCE_ID_IMAGE2,RESOURCE_ID_IMAGE3,RESOURCE_ID_IMAGE4,RESOURCE_ID_IMAGE5,RESOURCE_ID_IMAGE6,RESOURCE_ID_IMAGE7
    
};
static const int RESOURCE_IDS_GOOD[] = {
    RESOURCE_ID_IMAGE10,RESOURCE_ID_IMAGE11,RESOURCE_ID_IMAGE12,RESOURCE_ID_IMAGE13,RESOURCE_ID_IMAGE14,RESOURCE_ID_IMAGE15
    
};

static bool incFlag = true;
static int roadCnt = 0;

static AppSync s_sync;
static uint8_t s_sync_buffer[256];

enum WeatherKey {
    WEATHER_ICON_KEY = 0,         // TUPLE_INT
    WEATHER_TEMPERATURE_KEY = 1,  // TUPLE_CSTRING
    WEATHER_CITY_KEY = 2,         // TUPLE_CSTRING
    WEATHER_HUM_KEY = 3
};
#define CITY_KEY 4
static const uint32_t WEATHER_ICONS[] = {
    RESOURCE_ID_IMAGE_SUN, //0
    RESOURCE_ID_IMAGE_CLOUD, //1
    RESOURCE_ID_IMAGE_RAIN, //2
    RESOURCE_ID_IMAGE_SNOW //3
};

#define NUM_CITY_DEFAULT 1849846
static int cityId;

#if defined(PBL_RECT)
    #define DEL_X (180-144)/2
    #define DEL_Y (180-168)/2
#elif defined(PBL_ROUND)
    #define DEL_X 0
    #define DEL_Y 0
#endif

static void request_weather();


static void sync_error_callback(DictionaryResult dict_error, AppMessageResult app_message_error, void *context) {
    APP_LOG(APP_LOG_LEVEL_DEBUG, "App Message Sync Error: %d", app_message_error);
}

static void sync_tuple_changed_callback(const uint32_t key, const Tuple* new_tuple, const Tuple* old_tuple, void* context) {
    
    static char text[20];
    static char textCTY[20];
    static char textHUM[20];
    
    switch (key) {
        case WEATHER_ICON_KEY:
            if(new_tuple->value->uint8 == 4){
                layer_set_hidden((Layer *)imageWeather_layer, true);
                return;
            }
            if(image_state_weather != EMPTY_SLOT)gbitmap_destroy(image_weather);
            image_weather = gbitmap_create_with_resource(WEATHER_ICONS[new_tuple->value->uint8]);
            bitmap_layer_set_bitmap(imageWeather_layer, image_weather);
            image_state_weather = 1;
            layer_set_hidden((Layer *)imageWeather_layer, false);
            break;
            
        case WEATHER_TEMPERATURE_KEY:
            snprintf(text, sizeof(text),"TMP:\n%s",new_tuple->value->cstring);
            text_layer_set_text(textTMP_layer, text);
            break;
            
        case WEATHER_CITY_KEY:
            snprintf(textCTY, sizeof(textCTY),"LOC:\n%s",new_tuple->value->cstring);
            text_layer_set_text(textCTY_layer, textCTY);
            break;
        case WEATHER_HUM_KEY:
            snprintf(textHUM, sizeof(textHUM),"PRS:\n%s",new_tuple->value->cstring);
            text_layer_set_text(textHUM_layer, textHUM);
            break;
        case CITY_KEY:
            if(cityId == (int)new_tuple->value->int32)return;
            cityId = (int)new_tuple->value->int32;
            persist_write_int(CITY_KEY, new_tuple->value->int32);
            request_weather();
            break;
    }
}

static void request_weather() {
    
    APP_LOG(APP_LOG_LEVEL_DEBUG, "call");
    DictionaryIterator *iter;
    app_message_outbox_begin(&iter);
    
    if (!iter) {
        // Error creating outbound message
        return;
    }
    
    int value = cityId;
    dict_write_int(iter, CITY_KEY, &value, sizeof(int), true);
    dict_write_end(iter);
    
    app_message_outbox_send();
    
}

//image
static void update_layer(){
    
    if(charge){
        if(roadCnt != IMG_MAXNUM_GOOD)return;
        if(incFlag){
            cnt++;
            if(cnt > IMG_MAXNUM_GOOD-1){
                incFlag = false;
                cnt = IMG_MAXNUM_GOOD-2;
            }
        }
        else{
            cnt--;
            if(cnt < 0){ incFlag = true; cnt = 1; } } if(image_state_good != EMPTY_SLOT)bitmap_layer_set_bitmap(image_layer, image_good[cnt]); if(cnt == 0){ timer = app_timer_register(2000, update_layer, NULL); } else if(cnt == 5){ timer = app_timer_register(2000, update_layer, NULL); } else{ timer = app_timer_register(100, update_layer, NULL); } } else{ if(roadCnt != IMG_MAXNUM)return; cnt++; if(cnt > IMG_MAXNUM-1)cnt=0;

        if(image_state != EMPTY_SLOT)bitmap_layer_set_bitmap(image_layer, image_walk[cnt]);
        
        timer = app_timer_register(150, update_layer, NULL);
    }
    
    //layer_mark_dirty(bitmap_layer_get_layer(image_layer));
    
}

static void changeImage(bool charge){
    
    roadCnt = 0;
    cnt = 0;
    
    //good
    if(charge){
        if(image_state != EMPTY_SLOT){
            int n= 0;
            for(n=0;n<IMG_MAXNUM;n++){
                gbitmap_destroy(image_walk[n]);
            }
            image_state = EMPTY_SLOT;
        }
        
        layer_set_frame(bitmap_layer_get_layer(image_layer),(GRect) { .origin = { 35-DEL_X, 23-DEL_Y }, .size = { 100, 139 } });
        layer_set_frame(text_layer_get_layer(textYear_layer),(GRect) { .origin = { 5, 70+DEL_Y }, .size = { 50, 28 } });

        int n=0;
        for(n=0;n<IMG_MAXNUM_GOOD;n++){
            image_good[n] = gbitmap_create_with_resource(RESOURCE_IDS_GOOD[n]);
            roadCnt++;
        }
        image_state_good = 1;
        //layer_set_hidden((Layer *)imageWeather_layer, true);
    }
    //walk
    else{
        if(image_state_good != EMPTY_SLOT){
            int n= 0;
            for(n=0;n<IMG_MAXNUM_GOOD;n++){
                gbitmap_destroy(image_good[n]);
            }
            
            image_state_good = EMPTY_SLOT;
        }
        
        layer_set_frame(bitmap_layer_get_layer(image_layer),(GRect) { .origin = { 45-DEL_X, 23-DEL_Y }, .size = { 90, 139 } });
        layer_set_frame(text_layer_get_layer(textYear_layer),(GRect) { .origin = { 15-DEL_X/1.5, 50-DEL_Y }, .size = { 50, 28 } });

        int n=0;
        for(n=0;n<IMG_MAXNUM;n++){ image_walk[n] = gbitmap_create_with_resource(RESOURCE_IDS[n]); roadCnt++; } image_state = 1; //layer_set_hidden((Layer *)imageWeather_layer, false); } } //text time static void handle_minute_tick(struct tm *tick_time, TimeUnits units_changed) { static char timeText[] = "sun 00:00"; static char yearText[] = "2014\n12/30"; strftime(timeText, sizeof(timeText), "%a %T",tick_time); text_layer_set_text(textTime_layer, timeText); strftime(yearText, sizeof(yearText), "%G\n%m/%d",tick_time); text_layer_set_text(textYear_layer, yearText); if(tick_time->tm_min%30 == 0)request_weather();
    
}

//text battery
static void handle_battery(BatteryChargeState charge_state) {

    static char battery_text[12];
#ifdef DEBAG
    if(true){
#else
    if(charge_state.is_charging){
#endif
        if(!charge){
            charge = true;
            changeImage(charge);
        }
        snprintf(battery_text, sizeof(battery_text),"Charging");
    }
    else{
        if(charge){
            charge = false;
            changeImage(charge);
        }
        snprintf(battery_text, sizeof(battery_text), "BTR:%d/100",charge_state.charge_percent);
    }
    text_layer_set_text(textBt_layer, battery_text);
    
}

static void window_load(Window *window) {
  
    Layer *window_layer = window_get_root_layer(window);
    
//main img
    BatteryChargeState charge_state = battery_state_service_peek();
    charge = charge_state.is_charging;
    int n=0;
    if(charge){
        for(n=0;n<IMG_MAXNUM_GOOD;n++){
            image_good[n] = gbitmap_create_with_resource(RESOURCE_IDS_GOOD[n]);
            roadCnt++;
        }
        image_state_good = 1;
        image_layer = bitmap_layer_create((GRect) { .origin = { 35-DEL_X, 23-DEL_Y }, .size = { 100, 139 } });
    }
    else{
        for(n=0;n<IMG_MAXNUM;n++){
            image_walk[n] = gbitmap_create_with_resource(RESOURCE_IDS[n]);
            roadCnt++;
        }
        image_state = 1;
        image_layer = bitmap_layer_create((GRect) { .origin = { 45-DEL_X, 23-DEL_Y }, .size = { 90, 139 } });
    }
    
    bitmap_layer_set_background_color(image_layer, GColorClear);
    layer_add_child(window_layer, bitmap_layer_get_layer(image_layer));
    
    //weather icon
    imageWeather_layer = bitmap_layer_create((GRect) { .origin = { 125-DEL_X/2, 35+DEL_Y*7 }, .size = { 30, 30 } });
    bitmap_layer_set_background_color(imageWeather_layer, GColorClear);
    layer_add_child(window_layer, bitmap_layer_get_layer(imageWeather_layer));
    layer_set_hidden((Layer *)imageWeather_layer, true);
    
//text
    //time
    textTime_layer = text_layer_create((GRect) { .origin = { 0, 5-DEL_Y }, .size = { 180-DEL_X*2, 18 } });
    text_layer_set_text_alignment(textTime_layer, GTextAlignmentCenter);
    text_layer_set_background_color(textTime_layer, GColorClear);
    text_layer_set_text_color(textTime_layer, GColorGreen);
    text_layer_set_font(textTime_layer,fonts_get_system_font(FONT_KEY_GOTHIC_18_BOLD));
    
    layer_add_child(window_layer, text_layer_get_layer(textTime_layer));
    
    //year
    textYear_layer = text_layer_create((GRect) { .origin = { 15-DEL_X/1.5, 50-DEL_Y }, .size = { 50, 28 } });
    if(charge)layer_set_frame(text_layer_get_layer(textYear_layer),(GRect) { .origin = { 5, 70 }, .size = { 50, 28 } });
    text_layer_set_text_alignment(textYear_layer, GTextAlignmentLeft);
    text_layer_set_background_color(textYear_layer, GColorClear);
    text_layer_set_text_color(textYear_layer, GColorGreen);
    
    layer_add_child(window_layer, text_layer_get_layer(textYear_layer));
    
    //battery
    textBt_layer = text_layer_create((GRect) { .origin = { 0, 155-DEL_Y }, .size = { 180-DEL_X*2, 18 } });
    text_layer_set_text_alignment(textBt_layer, GTextAlignmentCenter);
    text_layer_set_background_color(textBt_layer, GColorClear);
    text_layer_set_text_color(textBt_layer, GColorGreen);
    //handle_battery(battery_state_service_peek());
    
    layer_add_child(window_layer, text_layer_get_layer(textBt_layer));
    
    //temp
    textTMP_layer = text_layer_create((GRect) { .origin = { 132-DEL_X*1.8, 67-DEL_Y*7 }, .size = { 60, 42 } });
    text_layer_set_text_alignment(textTMP_layer, GTextAlignmentLeft);
    text_layer_set_background_color(textTMP_layer, GColorClear);
    text_layer_set_text_color(textTMP_layer, GColorGreen);
    text_layer_set_text(textTMP_layer, "TMP:NA");
    
    layer_add_child(window_layer, text_layer_get_layer(textTMP_layer));
    
    //hum
    textHUM_layer = text_layer_create((GRect) { .origin = { 130-DEL_X, 115 }, .size = { 70, 28 } });
    text_layer_set_text_alignment(textHUM_layer, GTextAlignmentLeft);
    text_layer_set_background_color(textHUM_layer, GColorClear);
    text_layer_set_text_color(textHUM_layer, GColorGreen);
    text_layer_set_text(textHUM_layer, "PRS:NA");
    
    layer_add_child(window_layer, text_layer_get_layer(textHUM_layer));
    
    //city
    textCTY_layer = text_layer_create((GRect) { .origin = { 10-DEL_X/3, 100 }, .size = { 70-DEL_X, 28 } });
    text_layer_set_text_alignment(textCTY_layer, GTextAlignmentLeft);
    text_layer_set_background_color(textCTY_layer, GColorClear);
    text_layer_set_text_color(textCTY_layer, GColorGreen);
    text_layer_set_text(textCTY_layer, "LOC:NA");
    
    layer_add_child(window_layer, text_layer_get_layer(textCTY_layer));

    
    handle_battery(battery_state_service_peek());
    
    Tuplet initial_values[] = {
        TupletInteger(WEATHER_ICON_KEY, (uint8_t) 4),
        TupletCString(WEATHER_TEMPERATURE_KEY, "NA"),//"1234\u00B0C"),
        TupletCString(WEATHER_CITY_KEY, "NA"),
        TupletCString(WEATHER_HUM_KEY, "NA"),
        TupletInteger(CITY_KEY, (int32_t)cityId),
    };
    
    app_sync_init(&s_sync, s_sync_buffer, sizeof(s_sync_buffer),
        initial_values, ARRAY_LENGTH(initial_values),
        sync_tuple_changed_callback, sync_error_callback, NULL
    );
    
    //request_weather();
    app_timer_register(500 /* milliseconds */, request_weather, NULL);

}

static void window_unload(Window *window) {
    
    int n= 0;
    if(image_state != EMPTY_SLOT){
        for(n=0;n<IMG_MAXNUM;n++){
            gbitmap_destroy(image_walk[n]);
        }
    }
    if(image_state_good != EMPTY_SLOT){
        for(n=0;n<IMG_MAXNUM_GOOD;n++){
            gbitmap_destroy(image_good[n]);
        }
    }
    if(image_state_weather != EMPTY_SLOT){
        gbitmap_destroy(image_weather);
    }
    
    bitmap_layer_destroy(image_layer);
    bitmap_layer_destroy(imageWeather_layer);
    text_layer_destroy(textTime_layer);
    text_layer_destroy(textYear_layer);
    text_layer_destroy(textBt_layer);
    text_layer_destroy(textTMP_layer);
    text_layer_destroy(textCTY_layer);
    text_layer_destroy(textHUM_layer);
    
}

static void init(void) {
    
    cityId = persist_exists(CITY_KEY) ? persist_read_int(CITY_KEY) : NUM_CITY_DEFAULT;
    
    window = window_create();
    window_set_background_color(window, GColorBlack);
    window_set_window_handlers(window, (WindowHandlers) {
        .load = window_load,
        .unload = window_unload,
    });
    window_stack_push(window, true);
    battery_state_service_subscribe(&handle_battery);
    tick_timer_service_subscribe(MINUTE_UNIT, &handle_minute_tick);
    timer = app_timer_register(125 /* milliseconds */, update_layer, NULL);
    
    app_message_open(256, 256);
}

static void deinit(void) {
    
    tick_timer_service_unsubscribe();
    battery_state_service_unsubscribe();
    app_sync_deinit(&s_sync);
    window_destroy(window);
    
}

int main(void) {
    
  init();

  app_event_loop();
  deinit();
    
}

角形と丸型の画面サイズの違いを調整するために、各レイヤーの座標がめんどくさいことになってます。内容自体は前回とあまりかわりません。Pebble Timeで残りの寿命を表示するWatch Faceを作ってみた

お次はjsファイル

function iconFromWeatherId(weatherId) {
    if (weatherId < 600) {
        return 2;
    } else if (weatherId < 700) {
        return 3;
    } else if (weatherId < 800) {
        return 1;
    } else {
        return 0;
    }
}

var apiKey = "your key";
var cityID = null;


function fetchWeather(latitude, longitude,id) {
    var req = new XMLHttpRequest();
    if(id != null)req.open('GET', "http://api.openweathermap.org/data/2.5/weather?id=" + cityID
              + "&appid=" + apiKey + "&cnt=1", true);
    else{
        req.open('GET', "http://api.openweathermap.org/data/2.5/weather?" + "lat=" + latitude + "&lon=" + longitude
            + "&appid=" + apiKey + "&cnt=1", true);
    }
    req.onload = function(e) {
        if (req.readyState == 4) {
            if(req.status == 200) {
                console.log(req.responseText);
                
                var response = JSON.parse(req.responseText);
                var temperature = ""+Math.round(response.main.temp-273.15)+"\u00B0C\n"+Math.round(response.main.temp_min-273.15)+"/"+Math.round(response.main.temp_max-273.15)+"\u00B0C";
                var icon = iconFromWeatherId(response.weather.id);
                var city = response.name;
                var prs = Math.round(response.main.pressure);
                console.log(temperature);
                console.log(icon);
                console.log(city);
                console.log(prs);
                Pebble.sendAppMessage({
                                      "WEATHER_ICON_KEY":icon,
                                      "WEATHER_TEMPERATURE_KEY":temperature,
                                      "WEATHER_CITY_KEY":city,
                                      "WEATHER_HUM_KEY":""+prs}
                                      );
                
            } else {
                console.log("ErrorAjax");
            }
        }
    }
    req.send(null);
}


var locationOptions = { "timeout": 15000, "maximumAge": 60000 };
 
function locationSuccess(pos) {
    var coordinates = pos.coords;
    fetchWeather(coordinates.latitude, coordinates.longitude,null);
}

function locationError(err) {
    console.warn('location error (' + err.code + '): ' + err.message);
    fetchWeather(null, null, cityID);
}


function getLocation(){
    console.log("getLocation");
    locationWatcher = window.navigator.geolocation.getCurrentPosition(locationSuccess, locationError, locationOptions);
}

Pebble.addEventListener("ready", function(e) {
    console.log("connect!" + e.ready);
    //getLocation();
    console.log(e.type);
});

Pebble.addEventListener("appmessage", function(e) {
    window.navigator.geolocation.getCurrentPosition(locationSuccess, locationError,locationOptions);
    getLocation();
    console.log(e.type);
    console.log(e.payload.temperature);
    console.log("message!");
                        
    var dict = e.payload;
    if(dict['CITY_KEY']){
        cityID = dict['CITY_KEY'];
    }
    getLocation();
                        
});

Pebble.addEventListener("webviewclosed", function(e) {
    console.log("webview closed");
    console.log(e.type);
    console.log(e.response);
    var configuration = JSON.parse(decodeURIComponent(e.response));
    var dict = {};
    dict['CITY_KEY'] = configuration.city-0;
    Pebble.sendAppMessage(dict, function() {
        console.log('Send successful: ' + JSON.stringify(dict));
    },
    function() {
        console.log('Send failed!');
    });
    cityID = configuration.city-0;
    getLocation();

});

Pebble.addEventListener('showConfiguration', function(e) {
    Pebble.openURL('http://fieldwalking.jp/pebble/pip-boy/');
});

天気予報APIはOpenWeatherMapってとこを使ってますが、登録してAPI Keyを取得する必要があります。

ネタぎれ

これで作っためぼしいWatch Faceはほとんど公開しました。

あとは特にこれといった機能もないようなやつか、自分用アプリと連携したりするような物ばかりなので完全にネタぎれです。

アィディアください

コメント