#include #include #include #include #include #include #include #include #include #include #define APP_NAME "wm_pasori" #include #define DR_WAV_IMPLEMENTATION #include "dr_wav.h" #define APP_WIDTH 58 #define APP_HEIGHT 58 #define SCAN_INTERVAL_MS 700 #define PASORI_TIMEOUT_MS 200 #define SUICA_SERVICE_CODE 0x090f #define SUICA_BLOCK_NUMBER 0 #define SOUND_DIR "/usr/local/share/wm_pasori" typedef enum { DISPLAY_START, DISPLAY_WAIT, DISPLAY_BALANCE, DISPLAY_READ_ERROR, DISPLAY_NO_DEVICE } DisplayState; static pasori *reader = NULL; static DisplayState display_state = DISPLAY_START; static unsigned int current_balance = 0; static int current_error = 0; static XFontStruct *font_small = NULL; static XFontStruct *font_large = NULL; static int last_card_detected = 0; static void close_pasori(void) { if (reader != NULL) { pasori_close(reader); reader = NULL; } } static int open_pasori(void) { int result; if (reader != NULL) { return 0; } reader = pasori_open(); if (reader == NULL) { return -1; } result = pasori_init(reader); if (result != 0) { fprintf(stderr, "pasori_init: error %d\n", result); pasori_close(reader); reader = NULL; return -1; } pasori_set_timeout(reader, PASORI_TIMEOUT_MS); fprintf(stderr, "PaSoRi initialized\n"); return 0; } static void play_sound(const char *sound_file) { char path[256]; drwav wav; ao_device *dev = NULL; ao_sample_format fmt; int default_driver; static pid_t sound_pid = 0; int status; if (sound_pid > 0) { kill(sound_pid, SIGTERM); waitpid(sound_pid, &status, 0); sound_pid = 0; } snprintf(path, sizeof(path), "%s/%s", SOUND_DIR, sound_file); sound_pid = fork(); if (sound_pid != 0) return; ao_initialize(); default_driver = ao_default_driver_id(); memset(&fmt, 0, sizeof(fmt)); fmt.bits = 16; fmt.channels = 2; fmt.rate = 48000; fmt.byte_format = AO_FMT_NATIVE; if (!drwav_init_file(&wav, path, NULL)) { fprintf(stderr, "dr_wav: failed to open %s\n", path); _exit(1); } fmt.bits = wav.bitsPerSample; fmt.channels = wav.channels; fmt.rate = wav.sampleRate; dev = ao_open_live(default_driver, &fmt, NULL); if (!dev) { fprintf(stderr, "ao_open_live failed\n"); drwav_uninit(&wav); _exit(1); } size_t frames_read; int16_t pcm_buf[4096]; while ((frames_read = drwav_read_pcm_frames_s16(&wav, 4096 / wav.channels, pcm_buf)) > 0) { ao_play(dev, (char *)pcm_buf, (int)(frames_read * wav.channels * sizeof(int16_t))); } ao_close(dev); drwav_uninit(&wav); ao_shutdown(); _exit(0); } static void draw_centered_text(Drawable drawable, GC gc, XFontStruct *font, int y, const char *text) { int width; int x; if (font == NULL || text == NULL) { return; } width = XTextWidth( font, text, (int)strlen(text) ); x = (APP_WIDTH - width) / 2; if (x < 1) { x = 1; } XSetFont( DADisplay, gc, font->fid ); XDrawString( DADisplay, drawable, gc, x, y, text, (int)strlen(text) ); } static void redraw_display(void) { Pixmap back; XGCValues values; GC background_gc; GC border_dark_gc; GC border_light_gc; GC normal_gc; GC accent_gc; GC warning_gc; GC error_gc; GC dim_gc; char balance_text[32]; char error_text[32]; back = DAMakePixmap(); values.foreground = DAGetColor("#101418"); values.graphics_exposures = False; background_gc = XCreateGC( DADisplay, DAWindow, GCForeground | GCGraphicsExposures, &values ); XFillRectangle( DADisplay, back, background_gc, 0, 0, APP_WIDTH, APP_HEIGHT ); values.foreground = DAGetColor("#313943"); border_dark_gc = XCreateGC( DADisplay, DAWindow, GCForeground, &values ); /* * 内枠 */ values.foreground = DAGetColor("#56616d"); border_light_gc = XCreateGC( DADisplay, DAWindow, GCForeground, &values ); XDrawRectangle( DADisplay, back, border_dark_gc, 0, 0, APP_WIDTH - 1, APP_HEIGHT - 1 ); XDrawRectangle( DADisplay, back, border_light_gc, 1, 1, APP_WIDTH - 3, APP_HEIGHT - 3 ); /* * 残高表示 */ values.foreground = DAGetColor("#d0d7de"); normal_gc = XCreateGC( DADisplay, DAWindow, GCForeground, &values ); /* * 残高表示 */ values.foreground = DAGetColor("#74ff89"); accent_gc = XCreateGC( DADisplay, DAWindow, GCForeground, &values ); /* * 読み取り警告 */ values.foreground = DAGetColor("#ffd75f"); warning_gc = XCreateGC( DADisplay, DAWindow, GCForeground, &values ); /* * デバイスエラー */ values.foreground = DAGetColor("#ff6464"); error_gc = XCreateGC( DADisplay, DAWindow, GCForeground, &values ); values.foreground = DAGetColor("#69737d"); dim_gc = XCreateGC( DADisplay, DAWindow, GCForeground, &values ); switch (display_state) { case DISPLAY_BALANCE: snprintf( balance_text, sizeof(balance_text), "%u", current_balance ); draw_centered_text( back, accent_gc, font_small, 13, "Suica" ); draw_centered_text( back, accent_gc, font_large, 36, balance_text ); draw_centered_text( back, dim_gc, font_small, 51, "YEN" ); break; case DISPLAY_WAIT: draw_centered_text( back, normal_gc, font_small, 15, "Suica" ); draw_centered_text( back, normal_gc, font_small, 33, "TOUCH" ); draw_centered_text( back, dim_gc, font_small, 49, "----" ); break; case DISPLAY_READ_ERROR: snprintf( error_text, sizeof(error_text), "ERR %d", current_error ); draw_centered_text( back, warning_gc, font_small, 15, "Suica" ); draw_centered_text( back, warning_gc, font_small, 33, "READ" ); draw_centered_text( back, warning_gc, font_small, 49, error_text ); break; case DISPLAY_NO_DEVICE: draw_centered_text( back, error_gc, font_small, 15, "PaSoRi" ); draw_centered_text( back, error_gc, font_small, 33, "NO DEV" ); draw_centered_text( back, dim_gc, font_small, 49, "USB?" ); break; case DISPLAY_START: default: draw_centered_text( back, normal_gc, font_small, 18, "PaSoRi" ); draw_centered_text( back, dim_gc, font_small, 39, "START" ); break; } /* * DockAppのicon windowへ反映する * * DASetPixmap()は渡した内容を表示側へ設定する */ DASetPixmap(back); XFreePixmap(DADisplay, back); XFreeGC(DADisplay, background_gc); XFreeGC(DADisplay, border_dark_gc); XFreeGC(DADisplay, border_light_gc); XFreeGC(DADisplay, normal_gc); XFreeGC(DADisplay, accent_gc); XFreeGC(DADisplay, warning_gc); XFreeGC(DADisplay, error_gc); XFreeGC(DADisplay, dim_gc); XFlush(DADisplay); } /* * Suica残高を読み取る * * 戻り値: * 0 成功 * 1 Suicaなし * -1 読取り失敗 * -2 PaSoRiなし・初期化失敗 */ static int read_suica_balance(unsigned int *balance, int *read_error) { felica *card; uint8_t block_data[16]; int result; size_t i; if (balance == NULL) { return -2; } *balance = 0; if (read_error != NULL) { *read_error = 0; } if (open_pasori() != 0) { return -2; } card = felica_polling( reader, FELICA_POLLING_SUICA, 0, 0 ); if (card == NULL) { return 1; } memset( block_data, 0, sizeof(block_data) ); result = felica_read_single( card, SUICA_SERVICE_CODE, 0, SUICA_BLOCK_NUMBER, block_data ); free(card); card = NULL; if (result != 0) { if (read_error != NULL) { *read_error = result; } fprintf( stderr, "felica_read_single: error %d\n", result ); return -1; } fprintf(stderr, "Suica block:"); for (i = 0; i < sizeof(block_data); i++) { fprintf(stderr, " %02x", block_data[i]); } fprintf(stderr, "\n"); /* * Suica利用履歴の残高。 * * オフセット10が下位バイト、 * オフセット11が上位バイト。 */ *balance = (unsigned int)block_data[10] | ((unsigned int)block_data[11] << 8); return 0; } /* * libdockappのタイマーコールバック。 */ static void timeout_callback(void) { unsigned int balance; int read_error; int result; DisplayState new_state; static int skip_next = 0; if (skip_next) { skip_next = 0; return; } result = read_suica_balance( &balance, &read_error ); switch (result) { case 0: new_state = DISPLAY_BALANCE; current_balance = balance; current_error = 0; if (!last_card_detected) { if (balance == 0) { play_sound("sound_02.wav"); } else if (balance <= 1000) { play_sound("sound_05.wav"); } else { play_sound("sound_03.wav"); } } last_card_detected = 1; skip_next = 1; break; case 1: new_state = DISPLAY_WAIT; current_error = 0; last_card_detected = 0; break; case -1: new_state = DISPLAY_READ_ERROR; current_error = read_error; if (!last_card_detected) { play_sound("sound_06.wav"); } last_card_detected = 1; skip_next = 1; break; default: new_state = DISPLAY_NO_DEVICE; current_error = 0; close_pasori(); last_card_detected = 0; break; } display_state = new_state; redraw_display(); } /* * 終了処理 */ static void destroy_callback(void) { close_pasori(); if (font_small != NULL) { XFreeFont(DADisplay, font_small); font_small = NULL; } if (font_large != NULL) { XFreeFont(DADisplay, font_large); font_large = NULL; } fprintf(stderr, "wm_pasori terminated\n"); } /* * メイン */ int main(int argc, char **argv) { DACallbacks callbacks = { destroy_callback, /* destroy */ NULL, /* buttonPress */ NULL, /* buttonRelease */ NULL, /* motion */ NULL, /* mouse enters window */ NULL, /* mouse leaves window */ timeout_callback /* timeout */ }; DAParseArguments( argc, argv, NULL, 0, "Window Maker DockApp for displaying Suica balance.\n", "wm_pasori 1.0" ); /* * FreeBSD libdockapp 0.7.2サンプル */ DASetExpectedVersion(20020126); DAOpenDisplay( NULL, argc, argv ); DACreateIcon( APP_NAME, APP_WIDTH, APP_HEIGHT, argc, argv ); font_small = XLoadQueryFont( DADisplay, "-misc-fixed-medium-r-normal--10-*-*-*-*-*-iso8859-1" ); if (font_small == NULL) { font_small = XLoadQueryFont( DADisplay, "fixed" ); } font_large = XLoadQueryFont( DADisplay, "-misc-fixed-bold-r-normal--18-*-*-*-*-*-iso8859-1" ); if (font_large == NULL) { font_large = XLoadQueryFont( DADisplay, "9x15bold" ); } if (font_large == NULL) { font_large = XLoadQueryFont( DADisplay, "fixed" ); } if (font_small == NULL || font_large == NULL) { fprintf(stderr, "Could not load X11 fonts\n"); close_pasori(); return EXIT_FAILURE; } DASetCallbacks(&callbacks); DASetTimeout(SCAN_INTERVAL_MS); display_state = DISPLAY_START; redraw_display(); DAShow(); timeout_callback(); /* * X11イベントとタイマー */ DAEventLoop(); return EXIT_SUCCESS; }