From 7268794f8edbe9c1eff5cec6edd7de46bb8d6a8b Mon Sep 17 00:00:00 2001 From: NeilBrown Date: Thu, 27 Dec 2012 10:14:56 +1100 Subject: [PATCH] Add lots of stuff I had been keeping elsewhere --- alarm/alarm.c | 275 ++++ alarm/cal.c | 1309 ++++++++++++++++ alarm/listsel.c | 579 ++++++++ alarm/listsel.h | 39 + alarm/wkalrm.c | 244 +++ contacts/contactdb.py | 157 ++ contacts/contacts.py | 619 ++++++++ gps-utils/gpstime | 33 + gps-utils/gpstz | 62 + gps-utils/ubxgen | 68 + gsm/BUG | 18 + gsm/TOFIX | 36 + gsm/atchan.py | 208 +++ gsm/gsm-carriers.py | 31 + gsm/gsm-data.py | 105 ++ gsm/gsm-getsms.py | 412 ++++++ gsm/gsm-sms.py | 186 +++ gsm/gsmd-old | 867 +++++++++++ gsm/gsmd.py | 893 +++++++++++ gsm/notes | 116 ++ icons/tapinput-dextr.png | Bin 0 -> 304 bytes lib/tap3 | 31 + lib/tapboard.py | 4 +- lib/tapboard_dextr.py | 438 ++++++ lib/wmctrl.py | 163 ++ netman/dnsmasq.conf | 3 + netman/interfaces | 47 + netman/netman.py | 737 ++++++++++ netman/sysctl.conf | 2 + netman/usbnet | 8 + netman/wifi-udhcpc.script | 72 + netman/wifinet | 16 + petrol/petrol.py | 437 ++++++ plato/cmd.py | 215 +++ plato/grouptypes.py | 105 ++ plato/plato.py | 564 +++++++ plato/plato_gsm.py | 484 ++++++ plato/plato_internal.py | 176 +++ plato/plato_settings.py | 34 + plato/plato_sms.py | 89 ++ plato/window_group.py | 66 + scribble/Sample-Pages/1 | 19 + scribble/Sample-Pages/2 | 23 + scribble/Sample-Pages/3 | 18 + scribble/scribble.desktop | 12 + scribble/scribble.png | Bin 0 -> 10593 bytes scribble/scribble.py | 1493 +++++++++++++++++++ scribble/scribble/Makefile | 13 + scribble/scribble/Sample-Pages/1 | 19 + scribble/scribble/Sample-Pages/2 | 23 + scribble/scribble/Sample-Pages/3 | 18 + scribble/scribble/scribble.desktop | 12 + scribble/scribble/scribble.png | Bin 0 -> 10593 bytes scribble/scribble/scribble.py | 1493 +++++++++++++++++++ shop/shop.py | 2210 ++++++++++++++++++++++++++++ sms/exesms | 48 + sms/sendsms.py | 1614 ++++++++++++++++++++ sms/storesms.py | 486 ++++++ sound/list.h | 289 ++++ sound/notes | 102 ++ sound/sound.c | 816 ++++++++++ utils/dialer.py | 334 +++++ utils/tapinput-dextr.py | 145 ++ utils/tapinput.py | 3 +- 64 files changed, 19135 insertions(+), 3 deletions(-) create mode 100644 alarm/alarm.c create mode 100644 alarm/cal.c create mode 100644 alarm/listsel.c create mode 100644 alarm/listsel.h create mode 100644 alarm/wkalrm.c create mode 100644 contacts/contactdb.py create mode 100644 contacts/contacts.py create mode 100755 gps-utils/gpstime create mode 100755 gps-utils/gpstz create mode 100755 gps-utils/ubxgen create mode 100644 gsm/BUG create mode 100644 gsm/TOFIX create mode 100644 gsm/atchan.py create mode 100755 gsm/gsm-carriers.py create mode 100644 gsm/gsm-data.py create mode 100644 gsm/gsm-getsms.py create mode 100644 gsm/gsm-sms.py create mode 100755 gsm/gsmd-old create mode 100644 gsm/gsmd.py create mode 100644 gsm/notes create mode 100644 icons/tapinput-dextr.png create mode 100644 lib/tap3 create mode 100644 lib/tapboard_dextr.py create mode 100644 lib/wmctrl.py create mode 100644 netman/dnsmasq.conf create mode 100644 netman/interfaces create mode 100644 netman/netman.py create mode 100644 netman/sysctl.conf create mode 100644 netman/usbnet create mode 100755 netman/wifi-udhcpc.script create mode 100644 netman/wifinet create mode 100644 petrol/petrol.py create mode 100644 plato/cmd.py create mode 100644 plato/grouptypes.py create mode 100644 plato/plato.py create mode 100644 plato/plato_gsm.py create mode 100644 plato/plato_internal.py create mode 100644 plato/plato_settings.py create mode 100644 plato/plato_sms.py create mode 100644 plato/window_group.py create mode 100755 scribble/Sample-Pages/1 create mode 100755 scribble/Sample-Pages/2 create mode 100755 scribble/Sample-Pages/3 create mode 100644 scribble/scribble.desktop create mode 100644 scribble/scribble.png create mode 100755 scribble/scribble.py create mode 100644 scribble/scribble/Makefile create mode 100755 scribble/scribble/Sample-Pages/1 create mode 100755 scribble/scribble/Sample-Pages/2 create mode 100755 scribble/scribble/Sample-Pages/3 create mode 100644 scribble/scribble/scribble.desktop create mode 100644 scribble/scribble/scribble.png create mode 100755 scribble/scribble/scribble.py create mode 100755 shop/shop.py create mode 100644 sms/exesms create mode 100755 sms/sendsms.py create mode 100644 sms/storesms.py create mode 100644 sound/list.h create mode 100644 sound/notes create mode 100644 sound/sound.c create mode 100644 utils/dialer.py create mode 100644 utils/tapinput-dextr.py diff --git a/alarm/alarm.c b/alarm/alarm.c new file mode 100644 index 0000000..c5b9403 --- /dev/null +++ b/alarm/alarm.c @@ -0,0 +1,275 @@ + +/* + * generate alarm messages as required. + * Alarm information is stored in /data/alarms + * format is: + * date:recurrence:message + * + * date is yyyy-mm-dd-hh-mm-ss + * recurrence is currently nndays + * message is any text + * + * When the time comes, a txt message is generated + * + * We use dnotify to watch the file + * + * We never report any event that is before the timestamp + * on the file - that guards against replaying lots of + * events if the clock gets set backwards. + */ + +#define _XOPEN_SOURCE +#define _BSD_SOURCE +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include "libsus.h" + +struct alarm_ev { + struct alarm_ev *next; + time_t when; + long recur; + char *mesg; +}; + +void alarm_free(struct alarm_ev *ev) +{ + while (ev) { + struct alarm_ev *n = ev->next; + free(ev->mesg); + free(ev); + ev = n; + } +} + +void update_event(struct alarm_ev *ev, time_t now) +{ + /* make sure 'when' is in the future if possible */ + while (ev->when < now && ev->recur > 0) + ev->when += ev->recur; +} + +struct alarm_ev *event_parse(char *line) +{ + struct alarm_ev *rv; + char *recur, *mesg; + struct tm tm; + long rsec; + recur = strchr(line, ':'); + if (!recur) + return NULL; + *recur++ = 0; + mesg = strchr(recur, ':'); + if (!mesg) + return NULL; + *mesg++ = 0; + line = strptime(line, "%Y-%m-%d-%H-%M-%S", &tm); + if (!line) + return NULL; + rsec = atoi(recur); + if (rsec < 0) + return NULL; + rv = malloc(sizeof(*rv)); + rv->next = NULL; + tm.tm_isdst = -1; + rv->when = mktime(&tm); + rv->recur = rsec; + rv->mesg = strdup(mesg); + return rv; +} + +struct alarm_ev *event_load_soonest(char *file, time_t now) +{ + /* load the file and return only the soonest event at or after now */ + char line[2048]; + struct stat stb; + struct alarm_ev *rv = NULL, *ev; + FILE *f; + + f = fopen(file, "r"); + if (!f) + return NULL; + if (fstat(fileno(f), &stb) == 0 && + stb.st_mtime > now) + now = stb.st_mtime; + + while (fgets(line, sizeof(line), f) != NULL) { + char *eol = line + strlen(line); + if (eol > line && eol[-1] == '\n') + eol[-1] = 0; + + ev = event_parse(line); + if (!ev) + continue; + update_event(ev, now); + if (ev->when < now) { + alarm_free(ev); + continue; + } + if (rv == NULL) { + rv = ev; + continue; + } + if (rv->when < ev->when) { + alarm_free(ev); + continue; + } + alarm_free(rv); + rv = ev; + } + fclose(f); + return rv; +} + +struct alarm_ev *event_load_soonest_dir(char *dir, time_t now) +{ + DIR *d = opendir(dir); + struct dirent *de; + struct alarm_ev *rv = NULL; + + if (!d) + return NULL; + while ((de = readdir(d)) != NULL) { + char *p = NULL; + struct alarm_ev *e; + if (de->d_ino == 0) + continue; + if (de->d_name[0] == '.') + continue; + asprintf(&p, "%s/%s", dir, de->d_name); + e = event_load_soonest(p, now); + free(p); + if (e == NULL) + ; + else if (rv == NULL) + rv = e; + else if (rv->when < e->when) + alarm_free(e); + else { + alarm_free(rv); + rv = e; + } + } + closedir(d); + return rv; +} + +void event_deliver(struct alarm_ev *ev) +{ + char line[2048]; + char file[1024]; + struct tm *tm; + char *z; + char *m; + int f; + static time_t last_ev; + time_t now; + + if (!ev->mesg || !ev->mesg[0]) + return; + time(&now); + if (now <= last_ev) + now = last_ev + 1; + last_ev = now; + tm = gmtime(&now); + strftime(line, 2048, "%Y%m%d-%H%M%S", tm); + tm = localtime(&ev->when); + strftime(line+strlen(line), 1024, " ALARM %Y%m%d-%H%M%S", tm); + z = line + strlen(line); + sprintf(z, "%+03d alarm ", tm->tm_gmtoff/60/15); + + z = line + strlen(line); + m = ev->mesg; + + while (*m) { + switch (*m) { + default: + *z++ = *m; + break; + case ' ': + case '\t': + case '\n': + sprintf(z, "%%%02x", *m); + z += 3; + break; + } + m++; + } + *z++ = '\n'; + *z = 0; + + sprintf(file, "/data/SMS/%.6s", line); + f = open(file, O_WRONLY | O_APPEND | O_CREAT, 0600); + if (f >= 0) { + write(f, line, strlen(line)); + close(f); + f = open("/data/SMS/newmesg", O_WRONLY | O_APPEND | O_CREAT, + 0600); + if (f >= 0) { + line[16] = '\n'; + write(f, line, 17); + close(f); + } + f = open("/var/run/alert/alarm", O_WRONLY | O_CREAT, 0600); + if (f) { + write(f, "new\n", 4); + close(f); + } + /* Abort any current suspend cycle */ + suspend_abort(-1); + } +} + + +time_t now; +struct event *wkev; +int efd; +static void check_alarms(int fd, short ev, void *vp) +{ + struct alarm_ev *evt = event_load_soonest_dir("/data/alarms", now); + if (wkev && !vp) + wakealarm_destroy(wkev); + wkev = NULL; + while ((evt = event_load_soonest_dir("/data/alarms", now)) != NULL + && evt->when <= time(0)) { + event_deliver(evt); + now = evt->when + 1; + alarm_free(evt); + } + if (!evt) + return; + wkev = wakealarm_set(evt->when, check_alarms, (void*)1); + alarm_free(evt); + fcntl(efd, F_NOTIFY, DN_MODIFY|DN_CREATE|DN_RENAME); +} + +main(int argc, char *argv[]) +{ + struct event ev; + sigset_t set; + + efd = open("/data/alarms", O_DIRECTORY|O_RDONLY); + if (efd < 0) + exit(1); + + event_init(); + signal_set(&ev, SIGIO, check_alarms, NULL); + signal_add(&ev, NULL); + + fcntl(efd, F_NOTIFY, DN_MODIFY|DN_CREATE|DN_RENAME); + + time(&now); + check_alarms(0, 0, NULL); + + event_loop(0); + exit(0); +} diff --git a/alarm/cal.c b/alarm/cal.c new file mode 100644 index 0000000..3eeff79 --- /dev/null +++ b/alarm/cal.c @@ -0,0 +1,1309 @@ +#define _XOPEN_SOURCE +#define _GNU_SOURCE + +#include +#include + +#include +#include +#include +#include + +#include + +#include "listsel.h" + +struct event { + struct event *next; + time_t when, first; + long recur; + char *mesg; +}; + + +char *days[] = { "Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat" }; + +GtkWidget *calblist[42], *month, *date_display, *date_time_display; +GtkWidget *window, *clockw, *cal, *reasonw, *timerw; +GtkWidget *browse_buttons, *event_buttons, *move_buttons, *move_event; +GtkWidget *timer_display, *time_display; +GtkWidget *today_btn, *undelete_btn; +GtkWidget *once_btn, *daily_btn, *weekly_btn, *freq_buttons; +GtkWidget *timers_list; +GtkTextBuffer *reason_buffer; +time_t dlist[42], chosen_date; +int prev_mday, hour, minute; +int freq; + +struct event *evlist, *active_event; +struct event *deleted_event; +struct list_entry_text *alarm_entries; +time_t alarm_date = 0; +int evcnt = -1; +int moving = 0; +struct sellist *alarm_selector; + +char *filename = NULL; + +PangoLayout *layout; +GdkGC *colour = NULL; + +int size(struct list_entry *i, int *width, int*height) +{ + PangoRectangle ink, log; + struct list_entry_text *item = (void*)i; + + if (i->height) { + *width = item->true_width; + *height = i->height; + return 0; + } + pango_layout_set_text(layout, item->text, -1); + pango_layout_get_extents(layout, &ink, &log); + *width = log.width / PANGO_SCALE; + *height = log.height / PANGO_SCALE; + item->true_width = i->width = *width; + i->height = *height; + return 0; +} + +int render(struct list_entry *i, int selected, GtkWidget *d) +{ + PangoRectangle ink, log; + struct list_entry_text *item = (void*)i; + int x; + GdkColor col; + + pango_layout_set_text(layout, item->text, -1); + + if (colour == NULL) { + colour = gdk_gc_new(gtk_widget_get_window(d)); + gdk_color_parse("purple", &col); + gdk_gc_set_rgb_fg_color(colour, &col); + } + if (selected) { + gdk_color_parse("pink", &col); + gdk_gc_set_rgb_fg_color(colour, &col); + gdk_draw_rectangle(gtk_widget_get_window(d), + colour, TRUE, + item->head.x, item->head.y, + item->head.width, item->head.height); + gdk_color_parse("purple", &col); + gdk_gc_set_rgb_fg_color(colour, &col); + } +#if 0 + x = (i->width - item->true_width)/2; + if (x < 0) + x = 0; +#else + x = 0; +#endif + gdk_draw_layout(gtk_widget_get_window(d), + colour, + item->head.x+x, item->head.y, layout); + return 0; +} + + + +void set_cal(time_t then); + +void set_date(GtkButton *button, int num) +{ + set_cal(dlist[num]); +} + +void prev_year(GtkButton *button) +{ + set_cal(chosen_date - 365*24*3600); +} +void next_year(GtkButton *button) +{ + set_cal(chosen_date + 365*24*3600); +} + +void finish(void) +{ + gtk_main_quit(); +} + +void set_time(GtkButton *button, int num) +{ + struct tm now; + time_t then; + char buf[100]; + int hr24, hr12; + char m = 'A'; + if (num >= 100) + hour = num/100; + else + minute = num; + + hr24 = hour; + if (hour == 24) + hr24 = 0; + hr12 = hour; + if (hr12 > 12) { + hr12 -= 12; + m = 'P'; + } + if (hr12 == 12) + m = 'P' + 'A' - m; + + then = chosen_date + hour * 3600 + minute * 60; + localtime_r(&then, &now); + strftime(buf, sizeof(buf), "%a, %d %B %Y, %H:%M", &now); + gtk_label_set_text(GTK_LABEL(date_display), buf); + gtk_label_set_text(GTK_LABEL(date_time_display), buf); +#if 0 + sprintf(buf, "%02d:%02dhrs\n%2d:%02d%cM", hr24, minute, + hr12, minute, m); + gtk_label_set_text(GTK_LABEL(time_label), buf); +#endif +} + +GtkWidget *add_button(GtkWidget **blist, char *name, + PangoFontDescription *fd, + GCallback handle) +{ + GtkWidget *l, *b; + if (*blist == NULL) { + *blist = gtk_hbox_new(TRUE, 0); + gtk_widget_show(*blist); + gtk_widget_set_size_request(*blist, -1, 80); + } + + l = *blist; + + b = gtk_button_new_with_label(name); + gtk_widget_show(b); + pango_font_description_set_size(fd, 12 * PANGO_SCALE); + gtk_widget_modify_font(GTK_BIN(b)->child, fd); + gtk_container_add(GTK_CONTAINER(l), b); + g_signal_connect((gpointer)b, "clicked", handle, NULL); + return b; +} +int delay = 0; +int show_time(void *d); +int timer_tick = -1; +void add_time(int mins); + +void load_timers(void); +void select_timer(void) +{ + gtk_container_remove(GTK_CONTAINER(window), cal); + show_time(NULL); + delay = 0; + add_time(0); + load_timers(); + gtk_container_add(GTK_CONTAINER(window), timerw); + timer_tick = g_timeout_add(1000, show_time, NULL); +} + +void events_select(void) +{ + /* the selected event becomes current and can be moved */ + if (!active_event) + return; + set_cal(active_event->when); +} + +void move_confirm(void) +{ + struct tm now; + time_t then; + char buf[100]; + + if (active_event) { + then = active_event->when; + localtime_r(&then, &now); + hour = now.tm_hour; + minute = now.tm_min; + } else + hour = minute = 0; + then = chosen_date + hour * 3600 + minute * 60; + localtime_r(&then, &now); + strftime(buf, sizeof(buf), "%a, %d %B %Y, %H:%M", &now); + gtk_label_set_text(GTK_LABEL(date_display), buf); + gtk_label_set_text(GTK_LABEL(date_time_display), buf); + + gtk_container_remove(GTK_CONTAINER(window), cal); + gtk_container_add(GTK_CONTAINER(window), clockw); +} + +void update_freq(void); +void events_move(void) +{ + if (!active_event) + return; + + moving = 1; + freq = active_event->recur; + set_cal(active_event->when); + + gtk_widget_hide(event_buttons); + gtk_widget_show(move_buttons); + update_freq(); + gtk_widget_show(freq_buttons); + + gtk_widget_hide(alarm_selector->drawing); + gtk_widget_show(move_event); + + gtk_text_buffer_set_text(reason_buffer, active_event->mesg, -1); +} + +void events_new(void) +{ + moving = 2; + freq = 0; + gtk_widget_hide(event_buttons); + gtk_widget_hide(browse_buttons); + gtk_widget_show(move_buttons); + update_freq(); + gtk_widget_show(freq_buttons); + + gtk_widget_hide(alarm_selector->drawing); + gtk_widget_show(move_event); + gtk_text_buffer_set_text(reason_buffer, "", -1); +} + +void move_abort(void) +{ + gtk_widget_hide(move_buttons); + gtk_widget_hide(freq_buttons); + if (active_event) { + gtk_widget_hide(browse_buttons); + gtk_widget_show(event_buttons); + } else { + gtk_widget_hide(event_buttons); + gtk_widget_show(browse_buttons); + } + + gtk_widget_show(alarm_selector->drawing); + gtk_widget_hide(move_event); + moving = 0; + if (active_event) + set_cal(active_event->when); + else + set_cal(chosen_date); + +} + +void cal_today(void) +{ + /* jump to today */ + time_t now; + time(&now); + set_cal(now); + if (deleted_event) { + gtk_widget_hide(today_btn); + gtk_widget_show(undelete_btn); + } +} + +void cal_restore(void) +{ + gtk_container_remove(GTK_CONTAINER(window), clockw); + gtk_container_add(GTK_CONTAINER(window), cal); +} + +void reason_confirm(void); +void clock_confirm(void) +{ + gtk_container_remove(GTK_CONTAINER(window), clockw); + gtk_container_add(GTK_CONTAINER(window), reasonw); + reason_confirm(); +} + +void update_freq(void) +{ + if (freq == 0) + gtk_label_set_markup(GTK_LABEL(GTK_BIN(once_btn)->child), "Once"); + else + gtk_label_set_markup(GTK_LABEL(GTK_BIN(once_btn)->child), "(Once)"); + + if (freq == 1*24*3600) + gtk_label_set_markup(GTK_LABEL(GTK_BIN(daily_btn)->child), "Daily"); + else if (freq == 2*24*3600) + gtk_label_set_markup(GTK_LABEL(GTK_BIN(daily_btn)->child), "2-Daily"); + else if (freq == 3*24*3600) + gtk_label_set_markup(GTK_LABEL(GTK_BIN(daily_btn)->child), "3-Daily"); + else + gtk_label_set_markup(GTK_LABEL(GTK_BIN(daily_btn)->child), "(Daily)"); + + if (freq == 7*24*3600) + gtk_label_set_markup(GTK_LABEL(GTK_BIN(weekly_btn)->child), "Weekly"); + else if (freq == 14*24*3600) + gtk_label_set_markup(GTK_LABEL(GTK_BIN(weekly_btn)->child), "Fortnightly"); + else + gtk_label_set_markup(GTK_LABEL(GTK_BIN(weekly_btn)->child), "(Weekly)"); +} +void set_once(void) +{ + freq = 0; + update_freq(); +} +void set_weekly(void) +{ + if (freq == 7 * 24 * 3600) + freq = 14 *24 * 3600; + else + freq = 7 * 24 * 3600; + update_freq(); +} +void set_daily(void) +{ + if (freq == 1 * 24 * 3600) + freq = 2 * 24 * 3600; + else if (freq == 2 *24 * 3600) + freq = 3 * 24 * 3600; + else + freq = 1 * 24 * 3600; + update_freq(); +} + +/********************************************************************/ + + +void event_free(struct event *ev) +{ + while (ev) { + struct event *n = ev->next; + free(ev->mesg); + free(ev); + ev = n; + } +} + +void update_event(struct event *ev, time_t now) +{ + /* make sure 'when' is in the future if possible */ + ev->when = ev->first; + while (ev->when < now && ev->recur > 0) + ev->when += ev->recur; +} + +void update_events(struct event *ev, time_t now) +{ + while (ev) { + update_event(ev, now); + ev = ev->next; + } +} + +void sort_events(struct event **evtp) +{ + /* sort events list in *evtp by ->when + * use merge sort + */ + int cnt=0; + + struct event *ev[2]; + ev[0] = *evtp; + ev[1] = NULL; + + do { + struct event **evp[2], *e[2]; + int current = 0; + time_t prev = 0; + int next = 0; + cnt++; + evp[0] = &ev[0]; + evp[1] = &ev[1]; + e[0] = ev[0]; + e[1] = ev[1]; + + /* take least of e[0] and e[1] + * if it is larger than prev, add to + * evp[current], else swap current then add + */ + while (e[0] || e[1]) { + if (e[next] == NULL || + (e[1-next] != NULL && + !((prev <= e[1-next]->when) + ^(e[1-next]->when <= e[next]->when) + ^(e[next]->when <= prev))) + ) + next = 1 - next; + + if (e[next]->when < prev) + current = 1 - current; + prev = e[next]->when; + *evp[current] = e[next]; + evp[current] = &e[next]->next; + e[next] = e[next]->next; + } + *evp[0] = NULL; + *evp[1] = NULL; + } while (ev[0] && ev[1]); + if (ev[0]) + *evtp = ev[0]; + else + *evtp = ev[1]; +} + +struct event *event_parse(char *line) +{ + struct event *rv; + char *recur, *mesg; + struct tm tm; + long rsec; + recur = strchr(line, ':'); + if (!recur) + return NULL; + *recur++ = 0; + mesg = strchr(recur, ':'); + if (!mesg) + return NULL; + *mesg++ = 0; + line = strptime(line, "%Y-%m-%d-%H-%M-%S", &tm); + if (!line) + return NULL; + rsec = atoi(recur); + if (rsec < 0) + return NULL; + rv = malloc(sizeof(*rv)); + rv->next = NULL; + tm.tm_isdst = -1; + rv->when = mktime(&tm); + rv->first = rv->when; + rv->recur = rsec; + rv->mesg = strdup(mesg); + return rv; +} + +struct event *event_load_all(char *file) +{ + /* load the file and return a linked list */ + char line[2048]; + struct event *rv = NULL, *ev; + FILE *f; + + f = fopen(file, "r"); + if (!f) + return NULL; + while (fgets(line, sizeof(line), f) != NULL) { + char *eol = line + strlen(line); + if (eol > line && eol[-1] == '\n') + eol[-1] = 0; + + ev = event_parse(line); + if (!ev) + continue; + ev->next = rv; + rv = ev; + } + return rv; +} + +void event_save_all(char *file, struct event *list) +{ + FILE *f; + char *tmp = strdup(file); + char *c; + + c = tmp + strlen(tmp); + while (c > tmp && c[-1] != '/') + c--; + if (*c) *c = '.'; + + f = fopen(tmp, "w"); + while (list) { + struct event *ev = list; + struct tm *tm; + char buf[100]; + list = ev->next; + + tm = localtime(&ev->first); + strftime(buf, sizeof(buf), "%Y-%m-%d-%H-%M-%S", tm); + fprintf(f, "%s:%d:%s\n", buf, ev->recur, ev->mesg); + } + fflush(f); + fsync(fileno(f)); + fclose(f); + rename(tmp, file); +} + + +void load_timers(void) +{ + time_t now = time(0); + struct event *timers = event_load_all("/data/alarms/timer"); + struct event *t; + char buf[1024]; + int len = 0; + + update_events(timers, now); + sort_events(&timers); + strcpy(buf,""); + for (t = timers ; t; t = t->next) { + struct tm *tm; + if (t->when < now) + continue; + tm = localtime(&t->when); + strftime(buf+len, sizeof(buf)-len, "%H:%M\n", tm); + len += strlen(buf+len); + } + event_free(timers); + gtk_label_set_text(GTK_LABEL(timers_list), buf); +} + + +struct list_entry *alarms_item(void *list, int n) +{ + struct event *ev; + + if (alarm_date != chosen_date) { + int i; + struct tm *tm, today; + + if (evlist == NULL) { + filename = "/home/neilb/ALARMS"; + evlist = event_load_all(filename); + if (evlist == NULL) { + filename = "/data/alarms/cal"; + evlist = event_load_all(filename); + } + } + update_events(evlist, chosen_date); + sort_events(&evlist); + evcnt = 0; + ev = evlist; + while (ev && ev->when < chosen_date) + ev = ev->next; + for ( ; ev ; ev = ev->next) + evcnt++; + free(alarm_entries); + alarm_entries = calloc(evcnt, sizeof(alarm_entries[0])); + ev = evlist; + while (ev && ev->when < chosen_date) + ev = ev->next; + tm = localtime(&chosen_date); + today = *tm; + for (i=0; ev ; i++, ev = ev->next) { + char buf[50], o, c, r; + struct tm *tm = localtime(&ev->when); + if (tm->tm_mday == today.tm_mday && + tm->tm_mon == today.tm_mon && + tm->tm_year == today.tm_year) + strftime(buf, 50, "%H:%M\t", tm); + else if (tm->tm_year == today.tm_year || + (tm->tm_year == today.tm_year + 1 && + tm->tm_mon < today.tm_mon) + ) + strftime(buf, 50, "%b%d\t", tm); + else + strftime(buf, 50, "%Y\t", tm); + + o = ' ';c=' '; + if (ev->when < time(0)) { + o = '('; + c = ')'; + } + if (ev->recur == 0) + r = ' '; + else if (ev->recur == 3600*24*1) + r = '1'; /* daily */ + else if (ev->recur == 3600*24*2) + r = '2'; /* 2-daily */ + else if (ev->recur == 3600*24*3) + r = '3'; /* 3-daily */ + else if (ev->recur == 3600*24*7) + r = '+'; /* weekly */ + else if (ev->recur == 3600*24*14) + r = '*'; /* fortnightly */ + else + r = '#'; /* other period */ + asprintf(&alarm_entries[i].text, "%c%c %s %s%c", + o, r, + buf, ev->mesg, c); + alarm_entries[i].bg = "white"; + alarm_entries[i].fg = "blue"; + alarm_entries[i].underline = 0; + } + alarm_date = chosen_date; + } + if (n >= evcnt) + return NULL; + + return &alarm_entries[n].head; +} + + +void events_delete(void) +{ + struct event **evp; + if (!active_event) + return; + + for (evp = &evlist; *evp; evp = &(*evp)->next) { + if (*evp != active_event) + continue; + *evp = active_event->next; + break; + } + if (deleted_event) { + free(deleted_event->mesg); + free(deleted_event); + } + deleted_event = active_event; + active_event = NULL; + event_save_all(filename, evlist); + alarm_date = 0; + move_abort(); +} + +void events_undelete(void) +{ + if (!deleted_event) + return; + active_event = deleted_event; + deleted_event = NULL; + active_event->next = evlist; + evlist = active_event; + event_save_all(filename, evlist); + alarm_date = 0; + move_abort(); +} + +void alarms_selected(void *list, int s) +{ + struct event *e; + + + if (s < 0 || s >= evcnt) + return; + e = evlist; + while (e && e->when < chosen_date) + e = e->next; + while (s && e) { + s--; + e = e->next; + } + active_event = e; + + gtk_widget_hide(browse_buttons); + gtk_widget_show(event_buttons); +} + +struct list_handlers alarms_han = { + .getitem = alarms_item, + .get_size = size, + .render = render, + .selected = alarms_selected, +}; + + +GtkWidget *create_cal_window(void) +{ + GtkWidget *v, *t, *l; + GtkWidget *h; + GtkWidget *blist = NULL; + int i,r,c; + PangoFontDescription *desc; + struct sellist *sl; + + desc = pango_font_description_new(); + pango_font_description_set_size(desc, 11 * PANGO_SCALE); + + v = gtk_vbox_new(FALSE, 0); + gtk_widget_show(v); + + h = gtk_hbox_new(FALSE, 0); + gtk_container_add_with_properties(GTK_CONTAINER(v), h, "expand", 0, NULL); + gtk_widget_show(h); + + l = gtk_button_new_with_label(" << "); + gtk_widget_modify_font(GTK_BIN(l)->child, desc); + gtk_button_set_relief(GTK_BUTTON(l), GTK_RELIEF_NONE); + g_signal_connect((gpointer)l, "clicked", + G_CALLBACK(prev_year), NULL); + gtk_container_add(GTK_CONTAINER(h), l); + gtk_widget_show(l); + gtk_widget_set(l, "can-focus", 0, NULL); + + l = gtk_label_new("Month 9999"); + gtk_widget_modify_font(l, desc); + gtk_container_add(GTK_CONTAINER(h), l); + gtk_widget_show(l); + month = l; + + + l = gtk_button_new_with_label(" >> "); + gtk_widget_modify_font(GTK_BIN(l)->child, desc); + gtk_button_set_relief(GTK_BUTTON(l), GTK_RELIEF_NONE); + g_signal_connect((gpointer)l, "clicked", + G_CALLBACK(next_year), NULL); + gtk_container_add(GTK_CONTAINER(h), l); + gtk_widget_show(l); + gtk_widget_set(l, "can-focus", 0, NULL); + + + t = gtk_table_new(7, 7, FALSE); + gtk_widget_show(t); + + gtk_container_add_with_properties(GTK_CONTAINER(v), t, "expand", 0, NULL); + + for (i=0; i<7; i++) { + l = gtk_label_new(days[i]); + gtk_widget_modify_font(l, desc); + gtk_widget_show(l); + gtk_table_attach(GTK_TABLE(t), l, i, i+1, 0, 1, GTK_EXPAND | GTK_FILL, GTK_FILL, 0, 0); + } + + for (r=1; r<7; r++) + for (c=0; c<7; c++) { + int p = (r-1)*7+c; + l = gtk_button_new_with_label(" 99 "); + gtk_widget_modify_font(GTK_BIN(l)->child, desc); + gtk_button_set_relief(GTK_BUTTON(l), GTK_RELIEF_NONE); + gtk_table_attach(GTK_TABLE(t), l, c,c+1, r,r+1, GTK_FILL, GTK_FILL, 0, 0); + gtk_widget_show(l); + g_signal_connect((gpointer)l, "clicked", + G_CALLBACK(set_date), (void*)(long)p); + calblist[p] = l; + dlist[p] = 0; + } + + /* list of alarms from this date */ + sl = listsel_new(NULL, &alarms_han); + pango_font_description_set_size(desc, 15 * PANGO_SCALE); + gtk_widget_modify_font(sl->drawing, desc); + gtk_container_add(GTK_CONTAINER(v), sl->drawing); + gtk_widget_show(sl->drawing); + alarm_selector = sl; + + l = gtk_text_view_new_with_buffer(reason_buffer); + gtk_widget_modify_font(l, desc); + gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(l), GTK_WRAP_WORD_CHAR); + gtk_widget_hide(l); + gtk_container_add(GTK_CONTAINER(v), l); + move_event = l; + + + add_button(&blist, "timer", desc, select_timer); + add_button(&blist, "new", desc, events_new); + today_btn = add_button(&blist, "today", desc, cal_today); + undelete_btn = add_button(&blist, "undelete", desc, events_undelete); + gtk_widget_hide(undelete_btn); + gtk_box_pack_end(GTK_BOX(v), blist, FALSE, FALSE, 0); + browse_buttons = blist; + blist = NULL; + + add_button(&blist, "go to", desc, events_select); + add_button(&blist, "change", desc, events_move); + add_button(&blist, "delete", desc, events_delete); + gtk_box_pack_end(GTK_BOX(v), blist, FALSE, FALSE, 0); + event_buttons = blist; + blist = NULL; + + add_button(&blist, "confirm", desc, move_confirm); + add_button(&blist, "abort", desc, move_abort); + gtk_widget_hide(blist); + gtk_box_pack_end(GTK_BOX(v), blist, FALSE, FALSE, 0); + + move_buttons = blist; + + blist = NULL; + once_btn = add_button(&blist, "Once", desc, set_once); + daily_btn = add_button(&blist, "(Daily)", desc, set_daily); + weekly_btn = add_button(&blist, "(Weekly)", desc, set_weekly); + gtk_widget_hide(blist); + gtk_box_pack_end(GTK_BOX(v), blist, FALSE, FALSE, 0); + freq_buttons = blist; + + return v; +} + +void set_cal(time_t then) +{ + struct tm now, first, today, *tm; + int d, x; + time_t today_s; + char buf[400]; + + time(&today_s); + localtime_r(&today_s, &today); + localtime_r(&then, &now); + + then -= now.tm_sec; + then -= now.tm_min*60; + then -= now.tm_hour*3600; + chosen_date = then; + + localtime_r(&then, &now); + + tm = localtime(&then); + + strftime(buf, sizeof(buf), "%a, %d %B %Y", &now); + gtk_label_set_text(GTK_LABEL(date_display), buf); + + /* previous month */ + while (tm->tm_mon == now.tm_mon) { + then -= 22*3600; + tm = localtime(&then); + } + /* Start of week */ + while (tm->tm_wday != 0) { + then -= 22*3600; + tm = localtime(&then); + } + first = *tm; + + if (abs(dlist[0] - then) > 48*3600) { + strftime(buf, 40, "%B %Y", &now); + gtk_label_set_text(GTK_LABEL(month), buf); + } + + for (d=0; d<42; d++) { + char *bg = "", *fg = "black", *today_fg = "blue"; + + if (tm->tm_mon != now.tm_mon) { + fg = "grey"; today_fg = "pink"; + } else if (tm->tm_mday == now.tm_mday) + bg = "background=\"green\""; + + if (tm->tm_year == today.tm_year && + tm->tm_mon == today.tm_mon && + tm->tm_mday == today.tm_mday) + fg = today_fg; + + sprintf(buf, " %02d ", + bg, fg, tm->tm_mday); + if (abs(dlist[d] - then) > 48*3600 || + tm->tm_mday == now.tm_mday || + tm->tm_mday == prev_mday) + gtk_label_set_markup(GTK_LABEL(GTK_BIN(calblist[d])->child), buf); + dlist[d] = then; + x = tm->tm_mday; + while (x == tm->tm_mday) { + then += 22*3600; + tm = localtime(&then); + } + } + prev_mday = now.tm_mday; + + alarm_selector->selected = -1; + if (!moving) { + active_event = NULL; + gtk_widget_hide(event_buttons); + gtk_widget_show(browse_buttons); + } + g_signal_emit_by_name(alarm_selector->drawing, "configure-event", + NULL, alarm_selector); + + + gtk_widget_show(today_btn); + gtk_widget_hide(undelete_btn); +} + +void center_me(GtkWidget *label, void *xx, int pos) +{ + /* I have just been realised. Find size and + * adjust position + */ + GtkWidget *button = gtk_widget_get_parent(label); + GtkWidget *parent = gtk_widget_get_parent(button); + GtkAllocation alloc; + int x = pos / 10000; + int y = pos % 10000; + + gtk_widget_get_allocation(button, &alloc); + printf("move %d %d/%d by %d/%d from %d/%d\n", pos, x, y, + alloc.width/2, alloc.height/2, alloc.x, alloc.y); + x -= alloc.width / 2; + y -= alloc.height / 2; + if (x != alloc.x || y != alloc.y) { + printf("move %d %d/%d by %d/%d from %d/%d\n", pos, x, y, + alloc.width/2, alloc.height/2, alloc.x, alloc.y); + gtk_fixed_move(GTK_FIXED(parent), button, x, y); + } +} + + +void add_nums(GtkWidget *f, int radius, int start, int end, int step, + int div, int scale, + char *colour, PangoFontDescription *desc) +{ + int i; + for (i=start; i%02d", colour, i); + l = gtk_button_new_with_label(" 99 "); + gtk_widget_modify_font(GTK_BIN(l)->child, desc); + gtk_label_set_markup(GTK_LABEL(GTK_BIN(l)->child), + buf); + gtk_widget_show(l); + scaled = i * scale; + g_signal_connect((gpointer)l, "clicked", + G_CALLBACK(set_time), (void*)scaled); + a = i * 2.0 * M_PI / div; + s = sin(a); c= cos(a); + r = (double)radius; + if (fabs(s) < fabs(c)) + r = r / fabs(c); + else + r = r / fabs(s); + x = 210 + (int)(r * s) - r/18; + y = 210 - (int)(r * c) - r/18; + gtk_fixed_put(GTK_FIXED(f), l, x, y); + } +} + +GtkWidget *create_clock_window(void) +{ + PangoFontDescription *desc; + GtkWidget *f, *l, *v; + GtkWidget *blist = NULL; + + desc = pango_font_description_new(); + + v = gtk_vbox_new(FALSE, 0); + gtk_widget_show(v); + + l = gtk_label_new(" Date "); /* Date for which time is being chosen */ + pango_font_description_set_size(desc, 12 * PANGO_SCALE); + gtk_widget_modify_font(l, desc); + gtk_container_add_with_properties(GTK_CONTAINER(v), l, "expand", 0, NULL); + gtk_widget_show(l); + date_display = l; + + f = gtk_fixed_new(); + gtk_widget_show(f); + gtk_container_add(GTK_CONTAINER(v), f); + + pango_font_description_set_size(desc, 17 * PANGO_SCALE); + add_nums(f, 200, 6, 18, 1, 12, 100, "light green", desc); + pango_font_description_set_size(desc, 15 * PANGO_SCALE); + add_nums(f, 140, 1, 6, 1, 12, 100, "light blue", desc); + add_nums(f, 140,18,25, 1, 12, 100, "light blue", desc); + pango_font_description_set_size(desc, 12 * PANGO_SCALE); + add_nums(f, 95, 0, 60, 5, 60, 1, "pink", desc); + +#if 0 + l = gtk_label_new("09:00\n9:00AM"); + hour = 9; + minute = 0; + pango_font_description_set_size(desc, 12 * PANGO_SCALE); + gtk_widget_modify_font(l, desc); + gtk_widget_show(l); + gtk_fixed_put(GTK_FIXED(f), l, 170,180); + time_label = l; +#endif + + add_button(&blist, "confirm", desc, clock_confirm); + add_button(&blist, "back", desc, cal_restore); + gtk_box_pack_end(GTK_BOX(v), blist, FALSE, FALSE, 0); + + return v; +} + +char *reasons[] = {"awake", "go", "meet", "buy", "call", "doctor", "dentist", "ok", "thanks", "coming"}; +struct list_entry_text *reason_entries; +int selected_reason = -1; +int num_reasons; + +struct list_entry *item(void *list, int n) +{ + if (n < num_reasons) + return &reason_entries[n].head; + else + return NULL; +} +void selected(void *list, int n) +{ + selected_reason = n; +} + +struct list_handlers reason_han = { + .getitem = item, + .get_size = size, + .render = render, + .selected = selected, +}; + +void reason_back(void) +{ + gtk_container_remove(GTK_CONTAINER(window), reasonw); + gtk_container_add(GTK_CONTAINER(window), clockw); +} + +void reason_confirm(void) +{ + struct event *ev; + GtkTextIter start, finish; + + if (!active_event) { + ev = malloc(sizeof(*ev)); + ev->next = evlist; + evlist = ev; + } else { + ev = active_event; + free(ev->mesg); + } + ev->recur = freq; + ev->when = ev->first = chosen_date + hour*3600 + minute*60; + gtk_text_buffer_get_bounds(reason_buffer, &start, &finish); + ev->mesg = gtk_text_buffer_get_text(reason_buffer, + &start, &finish, FALSE); + gtk_container_remove(GTK_CONTAINER(window), reasonw); + gtk_container_add(GTK_CONTAINER(window), cal); + + event_save_all(filename, evlist); + + active_event = ev; + alarm_date = 0; /* force refresh */ + move_abort(); +} + +void reason_select(void) +{ + if (selected_reason < 0 || + selected_reason >= num_reasons) + return; + gtk_text_buffer_delete_selection(reason_buffer, TRUE, TRUE); + gtk_text_buffer_insert_at_cursor(reason_buffer, " ", 1); + gtk_text_buffer_insert_at_cursor(reason_buffer, reasons[selected_reason], + strlen(reasons[selected_reason])); +} + +GtkWidget *create_reason_window(void) +{ + /* The "reason" window allows the reason for the alarm + * to be set. + * It has: + * a label to show date and time + * a text editing widget containing the reason + * a selectable list of canned reasons + * buttons for: confirm select clear + * + * confirm adds the alarm, or not if the reason is clear + * select adds the selected canned reason to the editor + * clear clears the reason + */ + + PangoFontDescription *desc; + GtkWidget *v, *blist=NULL, *l; + struct sellist *sl; + int i, num; + + desc = pango_font_description_new(); + v = gtk_vbox_new(FALSE, 0); + gtk_widget_show(v); + + l = gtk_label_new(" Date and Time "); + pango_font_description_set_size(desc, 10 * PANGO_SCALE); + gtk_widget_modify_font(l, desc); + gtk_widget_show(l); + gtk_container_add_with_properties(GTK_CONTAINER(v), l, "expand", 0, NULL); + date_time_display = l; + + l = gtk_text_view_new_with_buffer(reason_buffer); + gtk_widget_modify_font(l, desc); + gtk_text_view_set_wrap_mode(GTK_TEXT_VIEW(l), GTK_WRAP_WORD_CHAR); + gtk_widget_show(l); + gtk_container_add(GTK_CONTAINER(v), l); + gtk_text_buffer_set_text(reason_buffer, "Sample Reason", -1); + + num = sizeof(reasons)/sizeof(reasons[0]); + reason_entries = calloc(num+1, sizeof(reason_entries[0])); + for (i=0; idrawing, desc); + gtk_container_add(GTK_CONTAINER(v), sl->drawing); + gtk_widget_show(sl->drawing); + + add_button(&blist, "confirm", desc, reason_confirm); + add_button(&blist, "select", desc, reason_select); + add_button(&blist, "back", desc, reason_back); + gtk_box_pack_end(GTK_BOX(v), blist, FALSE, FALSE, 0); + return v; +} + +void add_time(int mins) +{ + time_t now; + char buf[30]; + struct tm *tm; + delay += mins; + if (delay <= 0) + delay = 0; + now = time(0) + (delay?:1)*60; + tm = localtime(&now); + sprintf(buf, "+%d:%02d - ", + delay/60, delay%60); + strftime(buf+strlen(buf), + sizeof(buf)-strlen(buf), + "%H:%M", tm); + gtk_label_set_text(GTK_LABEL(timer_display), buf); + +} +void add_time_60(void) { add_time(60); } +void add_time_10(void) { add_time(10); } +void add_time_1(void) { add_time(1); } +void del_time_60(void) { add_time(-60); } +void del_time_10(void) { add_time(-10); } +void del_time_1(void) { add_time(-1); } + +int show_time(void *d) +{ + time_t now; + char buf[30]; + struct tm *tm; + now = time(0); + tm = localtime(&now); + strftime(buf, sizeof(buf), "%H:%M:%S", tm); + gtk_label_set_text(GTK_LABEL(time_display), buf); +} + +void to_cal(void) +{ + gtk_container_remove(GTK_CONTAINER(window), timerw); + g_source_remove(timer_tick); + gtk_container_add(GTK_CONTAINER(window), cal); +} +void set_timer(void) +{ + FILE *f = fopen("/data/alarms/timer", "a"); + char buf[100]; + time_t now = time(0) + delay*60; + struct tm *tm = localtime(&now); + + strftime(buf, sizeof(buf), "%Y-%m-%d-%H-%M-%S::TIMER\n", tm); + if (f) { + fputs(buf, f); + fclose(f); + } + to_cal(); +} +void clear_timers(void) +{ + unlink("/data/alarms/timer"); + to_cal(); +} + +GtkWidget *create_timer_window(void) +{ + /* The timer window lets you set a one-shot alarm + * some number of minutes in the future. + * There are buttons for +1hr +10 +1 and - all those + * The timer window also shows a large digital clock with seconds + */ + + PangoFontDescription *desc; + GtkWidget *v, *blist, *l; + + desc = pango_font_description_new(); + v = gtk_vbox_new(FALSE, 0); + gtk_widget_show(v); + + l = gtk_label_new(" Timer "); + pango_font_description_set_size(desc, 25 * PANGO_SCALE); + gtk_widget_modify_font(l, desc); + gtk_widget_show(l); + gtk_container_add_with_properties(GTK_CONTAINER(v), l, "expand", 0, NULL); + + l = gtk_label_new(" 99:99:99 "); + pango_font_description_set_size(desc, 40 * PANGO_SCALE); + gtk_widget_modify_font(l, desc); + gtk_widget_show(l); + gtk_container_add_with_properties(GTK_CONTAINER(v), l, "expand", 0, NULL); + time_display = l; + + l = gtk_label_new(" "); + pango_font_description_set_size(desc, 10 * PANGO_SCALE); + gtk_widget_modify_font(l, desc); + gtk_widget_show(l); + gtk_container_add_with_properties(GTK_CONTAINER(v), l, "expand", 1, NULL); + timers_list = l; + + + /* now from the bottom up */ + blist = NULL; + pango_font_description_set_size(desc, 10 * PANGO_SCALE); + add_button(&blist, "Return", desc, to_cal); + add_button(&blist, "Set", desc, set_timer); + add_button(&blist, "Clear All", desc, clear_timers); + gtk_box_pack_end(GTK_BOX(v), blist, FALSE, FALSE, 0); + + blist = NULL; + pango_font_description_set_size(desc, 10 * PANGO_SCALE); + add_button(&blist, "-1hr", desc, del_time_60); + add_button(&blist, "-10m", desc, del_time_10); + add_button(&blist, "-1m", desc, del_time_1); + gtk_box_pack_end(GTK_BOX(v), blist, FALSE, FALSE, 0); + + + l = gtk_label_new("+1:34 - 99:99 "); + pango_font_description_set_size(desc, 15 * PANGO_SCALE); + gtk_widget_modify_font(l, desc); + gtk_widget_show(l); + + gtk_box_pack_end(GTK_BOX(v), l, FALSE, FALSE, 0); + timer_display = l; + + blist = NULL; + pango_font_description_set_size(desc, 10 * PANGO_SCALE); + add_button(&blist, "+1hr", desc, add_time_60); + add_button(&blist, "+10m", desc, add_time_10); + add_button(&blist, "+1m", desc, add_time_1); + gtk_box_pack_end(GTK_BOX(v), blist, FALSE, FALSE, 0); + + return v; +} + +main(int argc, char *argv[]) +{ + GtkSettings *set; + + gtk_set_locale(); + gtk_init_check(&argc, &argv); + + set = gtk_settings_get_default(); + gtk_settings_set_long_property(set, "gtk-xft-dpi", 151 * PANGO_SCALE, "code"); + + + window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + gtk_window_set_default_size(GTK_WINDOW(window), 480, 640); + { + PangoContext *context; + PangoFontDescription *desc; + desc = pango_font_description_new(); + + pango_font_description_set_size(desc, 12 * PANGO_SCALE); + gtk_widget_modify_font(window, desc); + context = gtk_widget_get_pango_context(window); + layout = pango_layout_new(context); + } + + reason_buffer = gtk_text_buffer_new(NULL); + cal = create_cal_window(); + clockw = create_clock_window(); + reasonw = create_reason_window(); + timerw = create_timer_window(); + gtk_container_add(GTK_CONTAINER(window), cal); + g_signal_connect((gpointer)window, "destroy", + G_CALLBACK(finish), NULL); + gtk_widget_show(window); + gtk_widget_ref(cal); + gtk_widget_ref(clockw); + gtk_widget_ref(reasonw); + gtk_widget_ref(timerw); + set_cal(time(0)); + + + + gtk_main(); +} diff --git a/alarm/listsel.c b/alarm/listsel.c new file mode 100644 index 0000000..d6e20f5 --- /dev/null +++ b/alarm/listsel.c @@ -0,0 +1,579 @@ +/* + * selectable, auto-scrolling list widget + * + * We display a list of texts and allow one to be selected, + * normally with a tap. + * The selected text is highlighted and the list auto-scrolls to + * ensure it is not too close to the edge, so no other scroll + * function is needed. + * + * The list is defined by a function that takes a number + * and returns the element. + * This element will point to a table of functions that + * measure the size of the entry and render it. Some standard + * functions are provided to help with this. + * Each element has space to store current size/location for + * tap lookups. + */ +#include +#include + +#include +#include + +#include + +#include "listsel.h" + +void calc_layout_no(struct sellist *list) +{ + /* choose an appropriate 'top', 'last' and cols + * set x,y,width,height on each entry in that range. + * + * Starting at 'sel' (or zero) we walk backwards + * towards 'top' assessing size. + * If we hit 'top' while in first 1/3 of display + * we target being just above bottom third + * If we don't reach 'top' while above bottom + * 1/3, then we target just below the top 1/3, + * having recorded where 'top' would be (When we first + * reached 1/3 from 'sel') + * Once we have chosen top, we move to end to find + * bottom. If we hit end before bottom, + * we need to move top backwards if possible. + * + * So: + * - walk back from 'sel' until find + * 'top' or distance above sel exceeds 2/3 + * record location where distance was last < 1/3 + * - if found top and distance < 1/3 and top not start, + * step back while that is still < 1/3 + * - elif distance exceeds 2/3, set top et al to the + * found 1/3 position + * - move down from 'sel' until total distance is + * height, or hit end. + * - if hit end, move top backwards while total distance + * still less than height. + * + * columns add a complication as we don't know how + * many columns to use until we know the max width of + * the display choices. + * I think we assume cols based on selected entry, + * and as soon as we find something that decreases + * col count, start again from top. + * + * That doesn't work so well, particularly with multiple + * columns - we don't see the col-breaks properly. + * To see them, we really need to chose a firm starting + * place. + * So: + * Start with 'top' at beginning and walk along. + * If we hit end of list before bottom and top!=0, + * walk backwards from end above bottom to set top. + * If we find 'sel' after first 1/3 and before last - good. + * If before first third, set 'sel' above last third + * and walk back until find beginning or start, set top there. + * If after last third, set 'sel' past first third, + * walk back to top, set top there. + * + */ + int top, last; + int sel; + int i; + int cols = 1, col, row; + struct list_entry *e; + int w, h; + int dist, pos, colwidth; + int altdist, alttop; + + sel = list->selected; + if (sel < 0) + sel = 0; + e = list->han->getitem(list->list, sel); + if (e == NULL && sel > 0) { + sel = 0; + e = list->han->getitem(list->list, sel); + } + if (e == NULL) + /* list is empty */ + return; + + top = list->top; + if (top < 0) + top = 0; + if (top > sel) + top = sel; + + retry_cols: + // did_scroll = 0; + retry_scroll: + /* lay things out from 'top' */ + i = top; col = 0; row = 0; + while (col < cols) { + e = list->han->getitem(list->list, i); + if (e == NULL) + break; + list->han->get_size(e, &w, &h); + e->x = col * list->width / cols; + e->y = row; + e->width = list->width / cols; + row += h; + if (row > list->height) { + col++; + e->x = col * list->width / cols; + e->y = 0; + row = h; + } + e->need_draw = 1; + } + + list->han->get_size(e, &w, &h); + cols = list->width / w; + if (cols == 0) + cols = 1; + col_retry: + dist = 0; + i = sel; + e = list->han->getitem(list->list, i); + list->han->get_size(e, &w, &h); + dist += h; + + while (i > top && dist < (cols-1) * list->height + 2 * list->height/3) { + //printf("X i=%d dist=%d cols=%d lh=%d at=%d\n", i, dist, cols, list->height, alttop); + i--; + e = list->han->getitem(list->list, i); + list->han->get_size(e, &w, &h); + dist += h; + if (cols > 1 && w*cols > list->width) { + cols --; + goto col_retry; + } + if (dist * 3 < list->height) { + alttop = i; + altdist = dist; + } + } + if (i == top) { + if (dist*3 < list->height) { + /* try to move to other end */ + while (i > 0 && + dist < (cols-1)*list->height * list->height*2/3) { + i--; + e = list->han->getitem(list->list, i); + list->han->get_size(e, &w, &h); + dist += h; + if (cols > 1 && w*cols > list->width) { + cols--; + goto col_retry; + } + top = i; + } + } + } else { + /* too near bottom */ + top = alttop; + dist = altdist; + } + + i = sel; + e = list->han->getitem(list->list, i); + list->han->get_size(e, &w, &h); + while (dist + h <= list->height * cols) { + dist += h; + i++; + //printf("Y i=%d dist=%d cols=%d\n", i, dist, cols); + e = list->han->getitem(list->list, i); + if (e == NULL) + break; + list->han->get_size(e, &w, &h); + if (cols > 1 && w*cols > list->width) { + cols --; + goto col_retry; + } + } + if (e == NULL) { + /* near end, maybe move top up */ + i = top; + while (i > 0) { + i--; + e = list->han->getitem(list->list, i); + list->han->get_size(e, &w, &h); + if (dist + h >= list->height * cols) + break; + dist += h; + if (cols > 1 && w * cols > list->width) { + cols--; + goto col_retry; + } + top = i; + } + } + + /* OK, we are going with 'top' and 'cols'. */ + list->cols = cols; + list->top = top; + list->selected = sel; + /* set x,y,width,height for each + * width and height are set - adjust width and set x,y + */ + col = 0; + pos = 0; + i = top; + colwidth = list->width / cols; + last = top; + while (col < cols) { + e = list->han->getitem(list->list, i); + if (e == NULL) + break; + e->x = col * colwidth; + e->y = pos; + //printf("Set %d,%d %d,%d\n", e->x, e->y, e->height, list->height); + pos += e->height; + if (pos > list->height) { + pos = 0; + col++; + continue; + } + e->width = colwidth; + e->need_draw = 1; + last = i; + i++; + } + list->last = last; +} + +void calc_layout(struct sellist *list) +{ + /* Choose 'top', and set 'last' and 'cols' + * so that 'selected' is visible in width/height, + * and is not in a 'bad' position. + * If 'sel' is in first column and ->y < height/3 + * and 'top' is not zero, that is bad, we set top to zero. + * If 'sel' is in last column and ->Y > height/3 + * and 'last' is not end of list, that is bad, we set + * top to 'last' + * If 'sel' is before 'top', that is bad, set 'top' to 'sel'. + * If 'sel' is after 'last', that is bad, set 'top' to 'sel'. + */ + int top = list->top; + int sel = list->selected; + int cols = 10000; + int col, row, pos, last; + struct list_entry *sele; + int did_jump = 0; + + retry_cols: + /* make sure 'top' and 'sel' are valid */ + top = list->top; + sel = list->selected; + did_jump = 0; + if (top < 0 || list->han->getitem(list->list, top) == NULL) + top = 0; + if (sel < 0 || list->han->getitem(list->list, sel) == NULL) + sel = 0; + + + retry: + //printf("try with top=%d cols=%d\n", top, cols); + pos = top; col=0; row=0; last= -1; + sele = NULL; + while(1) { + int w, h; + struct list_entry *e; + + e = list->han->getitem(list->list, pos); + if (e == NULL) + break; + if (pos == sel) + sele = e; + list->han->get_size(e, &w, &h); + if (cols > 1 && w * cols > list->width) { + /* too many columns */ + cols = list->width / w; + if (cols <= 0) + cols = 1; + goto retry_cols; + } + e->x = col * list->width / cols; + e->y = row; + e->width = list->width / cols; + e->need_draw = 1; + row += h; + if (row > list->height && e->y > 0) { + col ++; + if (col == cols) + break; + e->x = col * list->width / cols; + e->y = 0; + row = h; + } + last = pos; + pos++; + } + /* Check if this is OK */ + if (sele == NULL) { + //printf("no sel: %d %d\n", sel, top); + if (last <= top) + goto out; + if (sel < top) + top--; + else + top++; + goto retry; + } + //printf("x=%d y=%d hi=%d lh=%d top=%d lw=%d cols=%d\n", + // sele->x, sele->y, sele->height, list->height, top, list->width, cols); + if (sele->x == 0 && sele->y + sele->height < list->height / 3 + && top > 0) { + if (did_jump) + top --; + else { + top = 0; + did_jump = 1; + } + goto retry; + } + if (sele->x == (cols-1) * list->width / cols && + sele->y + sele->height > list->height * 2 / 3 && + col == cols) { + if (did_jump) + top ++; + else { + top = last; + did_jump = 1; + } + goto retry; + } + if (col < cols && top > 0 && + (col < cols - 1 || row < list->height / 2)) { + top --; + goto retry; + } + out: + list->top = top; + list->last = last; + list->cols = cols; + //printf("top=%d last=%d cols=%d\n", top, last, cols); +} + + +void configure(GtkWidget *draw, void *event, struct sellist *list) +{ + GtkAllocation alloc; + + gtk_widget_get_allocation(draw, &alloc); +#if 0 + if (list->width == alloc.width && + list->height == alloc.height) + return; +#endif + list->width = alloc.width; + list->height = alloc.height; + calc_layout(list); + gtk_widget_queue_draw(draw); +} + +void expose(GtkWidget *draw, GdkEventExpose *event, struct sellist *list) +{ + int i; + /* Draw all fields in the event->area */ + + for (i = list->top; i <= list->last; i++) { + struct list_entry *e = list->han->getitem(list->list, i); + if (e->x > event->area.x + event->area.width) + /* to the right */ + continue; + if (e->x + e->width < event->area.x) + /* to the left */ + continue; + if (e->y > event->area.y + event->area.height) + /* below */ + continue; + if (e->y + e->height < event->area.y) + /* above */ + continue; + + e->need_draw = 1; + if (event->count == 0 && e->need_draw == 1) { + e->need_draw = 0; + list->han->render(e, i == list->selected, list->drawing); + } + } +} + +void tap(GtkWidget *draw, GdkEventButton *event, struct sellist *list) +{ + int i; + for (i=list->top; i <= list->last; i++) { + struct list_entry *e = list->han->getitem(list->list, i); + if (event->x >= e->x && + event->x < e->x + e->width && + event->y >= e->y && + event->y < e->y + e->height) + { + //printf("found item %d\n", i); + list->selected = i; + calc_layout(list); + gtk_widget_queue_draw(list->drawing); + list->han->selected(list, i); + break; + } + } +} + +void *listsel_new(void *list, struct list_handlers *han) +{ + struct sellist *rv; + + rv = malloc(sizeof(*rv)); + rv->list = list; + rv->han = han; + rv->drawing = gtk_drawing_area_new(); + rv->top = 0; + rv->selected = -1; + rv->width = rv->height = 0; + rv->cols = 1; + + g_signal_connect((gpointer)rv->drawing, "expose-event", + G_CALLBACK(expose), rv); + g_signal_connect((gpointer)rv->drawing, "configure-event", + G_CALLBACK(configure), rv); + + gtk_widget_add_events(rv->drawing, + GDK_BUTTON_PRESS_MASK| + GDK_BUTTON_RELEASE_MASK); + g_signal_connect((gpointer)rv->drawing, "button_release_event", + G_CALLBACK(tap), rv); + + return rv; +} + + +#ifdef MAIN + +struct list_entry_text *entries; +int entcnt; +PangoFontDescription *fd; +PangoLayout *layout; +GdkGC *colour; + +/* + * gdk_color_parse("green", GdkColor *col); + * gdk_colormap_alloc_color(GdkColormap, GdkColor, writeable?, bestmatch?) + * gdk_colormap_get_system + * + * gdk_gc_new(drawable) + * gdk_gc_set_foreground(gc, color) + * gdk_gc_set_background(gc, color) + * gdk_gc_set_rgb_fg_color(gc, color) + */ +struct list_entry *item(void *list, int n) +{ + if (n < entcnt) + return &entries[n].head; + else + return NULL; +} +int size(struct list_entry *i, int *width, int*height) +{ + PangoRectangle ink, log; + struct list_entry_text *item = (void*)i; + + if (i->height) { + *width = item->true_width; + *height = i->height; + return 0; + } + //printf("calc for %s\n", item->text); + pango_layout_set_text(layout, item->text, -1); + pango_layout_get_extents(layout, &ink, &log); + //printf("%s %d %d\n", item->text, log.width, log.height); + *width = log.width / PANGO_SCALE; + *height = log.height / PANGO_SCALE; + item->true_width = i->width = *width; + i->height = *height; + return 0; +} + +int render(struct list_entry *i, int selected, GtkWidget *d) +{ + PangoRectangle ink, log; + struct list_entry_text *item = (void*)i; + int x; + GdkColor col; + + pango_layout_set_text(layout, item->text, -1); + + if (colour == NULL) { + colour = gdk_gc_new(gtk_widget_get_window(d)); + gdk_color_parse("purple", &col); + gdk_gc_set_rgb_fg_color(colour, &col); + } + if (selected) { + gdk_color_parse("pink", &col); + gdk_gc_set_rgb_fg_color(colour, &col); + gdk_draw_rectangle(gtk_widget_get_window(d), + colour, TRUE, + item->head.x, item->head.y, + item->head.width, item->head.height); + gdk_color_parse("purple", &col); + gdk_gc_set_rgb_fg_color(colour, &col); + } + x = (i->width - item->true_width)/2; + if (x < 0) + x = 0; + gdk_draw_layout(gtk_widget_get_window(d), + colour, + item->head.x+x, item->head.y, layout); + return 0; +} +void selected(void *list, int n) +{ + printf("got %d\n", n); +} + +struct list_handlers myhan = { + .getitem = item, + .get_size = size, + .render = render, + .selected = selected, +}; + +main(int argc, char *argv[]) +{ + int i; + GtkWidget *w; + struct sellist *l; + PangoContext *context; + + entries = malloc(sizeof(*entries) * argc); + memset(entries, 0, sizeof(*entries)*argc); + for (i=1; idrawing); + gtk_widget_show(l->drawing); + gtk_widget_show(w); + + gtk_main(); +} +#endif diff --git a/alarm/listsel.h b/alarm/listsel.h new file mode 100644 index 0000000..0933deb --- /dev/null +++ b/alarm/listsel.h @@ -0,0 +1,39 @@ + +struct sellist { + void *list; + struct list_handlers *han; + GtkWidget *drawing; + + int top; /* Index of first element displayed */ + int selected; /* Index of currently selected element */ + int last; /* Index of last displayed element */ + + int width, height; /* Pixel size of widget */ + int cols; /* Columns */ +}; + +struct list_handlers { + struct list_entry *(*getitem)(void *list, int n); + int (*get_size)(struct list_entry *item, int *width, int *height); + int (*render)(struct list_entry *item, int selected, + GtkWidget *d); + void (*selected)(void *list, int element); +}; + +struct list_entry { + int x, y, width, height; + int need_draw; +}; + +struct list_entry_text { + struct list_entry head; + char *text; + + int true_width; + char *bg, *fg; + int underline; +}; + +extern void *listsel_new(void *list, struct list_handlers *han); + + diff --git a/alarm/wkalrm.c b/alarm/wkalrm.c new file mode 100644 index 0000000..89add78 --- /dev/null +++ b/alarm/wkalrm.c @@ -0,0 +1,244 @@ +/* + * wkalrm.c - Use the RTC alarm to wake us up + * + * Copyright (C) 2008 by OpenMoko, Inc. + * Written by Werner Almesberger + * All Rights Reserved + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + */ + + +#include +#include +#include +#include +#include +#include +#include +#include + + +#define DEFAULT_RTC "/dev/rtc0" + + +static const char *device = DEFAULT_RTC; +static int fd; + + +/* ----- Low-level wrappers ------------------------------------------------ */ + + +static void read_alarm(struct rtc_wkalrm *alarm) +{ + int res; + + res = ioctl(fd, RTC_WKALM_RD, alarm); + if (res < 0) { + perror("ioctl(RTC_WKALM_RD)"); + exit(1); + } +} + + +static void read_time(struct rtc_time *tm) +{ + int res; + + res = ioctl(fd, RTC_RD_TIME, tm); + if (res < 0) { + perror("ioctl(RTC_RD_TIME)"); + exit(1); + } +} + + +static void write_alarm(const struct rtc_wkalrm *alarm) +{ + int res; + + res = ioctl(fd, RTC_WKALM_SET, alarm); + if (res < 0) { + perror("ioctl(RTC_WKALM_SET)"); + exit(1); + } +} + + +/* ----- Date conversions -------------------------------------------------- */ + + +static void show_alarm(void) +{ + struct rtc_wkalrm alarm; + struct rtc_time tm; + + read_time(&tm); + printf("time is %02d:%02d:%02d %04d-%02d-%02d\n", + tm.tm_hour, tm.tm_min, tm.tm_sec, + tm.tm_year+1900, tm.tm_mon+1, + tm.tm_mday); + + + read_alarm(&alarm); + if (!alarm.enabled) + printf("alarm disabled%s\n", + alarm.pending ? " (pending)" : ""); + else + printf("%02d:%02d:%02d %04d-%02d-%02d%s\n", + alarm.time.tm_hour, alarm.time.tm_min, alarm.time.tm_sec, + alarm.time.tm_year+1900, alarm.time.tm_mon+1, + alarm.time.tm_mday, + alarm.pending ? " (pending)" : ""); +} + + +static void set_alarm_abs(const char *t, const char *day) +{ + fprintf(stderr, "not yet implemented :-)\n"); + exit(1); +} + + +static void set_alarm_delta(time_t delta) +{ + struct rtc_wkalrm alarm; + struct tm tm, *tmp; + time_t t; + + read_time(&alarm.time); + memset(&tm, 0, sizeof(tm)); + tm.tm_sec = alarm.time.tm_sec; + tm.tm_min = alarm.time.tm_min; + tm.tm_hour = alarm.time.tm_hour; + tm.tm_mday = alarm.time.tm_mday; + tm.tm_mon = alarm.time.tm_mon; + tm.tm_year = alarm.time.tm_year; + tm.tm_isdst = -1; + t = mktime(&tm); + if (t == (time_t) -1) { + fprintf(stderr, "mktime: error\n"); + exit(1); + } + t += delta; + tmp = localtime(&t); + if (!tmp) { + fprintf(stderr, "localtime_r: error\n"); + exit(1); + } + alarm.time.tm_sec = tmp->tm_sec; + alarm.time.tm_min = tmp->tm_min; + alarm.time.tm_hour = tmp->tm_hour; + alarm.time.tm_mday = tmp->tm_mday; + alarm.time.tm_mon = tmp->tm_mon; + alarm.time.tm_year = tmp->tm_year; + alarm.enabled = 1; + write_alarm(&alarm); +} + + +static void set_alarm_rel(const char *delta) +{ + unsigned long n; + char *end; + + n = strtoul(delta, &end, 10); + if (!strcmp(end, "d") || !strcmp(end, "day") || !strcmp(end, "days")) + n *= 24*3600; + else if (!strcmp(end, "h") || !strcmp(end, "hour") || + !strcmp(end, "hours")) + n *= 3600; + else if (!strcmp(end, "m") || !strcmp(end, "min") || + !strcmp(end, "mins")) + n *= 60; + else if (strcmp(end, "s") && strcmp(end, "sec") && + strcmp(end, "secs")) { + fprintf(stderr, "invalid delta time \"%s\"\n", delta); + exit(1); + } + set_alarm_delta(n); +} + + +static void disable_alarm(void) +{ + struct rtc_wkalrm alarm; + + read_alarm(&alarm); + alarm.enabled = 0; + write_alarm(&alarm); +} + + +static void set_alarm_24h(const char *t) +{ + fprintf(stderr, "not yet implemented :-)\n"); + exit(1); +} + + +static void set_alarm(const char *when) +{ + if (*when == '+') + set_alarm_rel(when+1); + else + set_alarm_24h(when); +} + + +/* ----- Command line parsing ---------------------------------------------- */ + + +static void usage(const char *name) +{ + fprintf(stderr, +"usage: %s [-d device]\n" +" %s [-d device] hh:mm[:ss] [[yyyy-]mm-dd]\n" +" %s [-d device] +Nunit\n\n" +" unit is d[ay[s]], h[our[s]] m[in[s]], or s[ec[s]]\n\n" +" -d device open the specified RTC device (default: %s)\n" + , name, name, name, DEFAULT_RTC); + exit(1); +} + + +int main(int argc, char **argv) +{ + int c; + + while ((c = getopt(argc, argv, "d:")) != EOF) + switch (c) { + case 'd': + device = optarg; + break; + default: + usage(*argv); + } + + fd = open(device, O_RDWR); + if (fd < 0) { + perror(device); + exit(1); + } + + switch (argc-optind) { + case 0: + show_alarm(); + break; + case 1: + if (!strcmp(argv[optind], "off")) + disable_alarm(); + else + set_alarm(argv[optind]); + break; + case 2: + set_alarm_abs(argv[optind], argv[optind+1]); + break; + default: + usage(*argv); + } + return 0; +} diff --git a/contacts/contactdb.py b/contacts/contactdb.py new file mode 100644 index 0000000..9e20ccf --- /dev/null +++ b/contacts/contactdb.py @@ -0,0 +1,157 @@ + +# +# Contacts DB interface for various programs +# Currently only understand name/number and speed-dials +# Separately we also provide access to log of incoming and +# outgoing calls, mapped to names through the contacts DB. +# +# Need: +# list of "speed-dials". These are ordered +# look up name/number from speed-dial +# map 'name' to entry - first match, with '.'s +# map 'number' to probable name +# load/save address book +# load /var/log/incoming and /var/log/outgoing +# + +import os, time + +class entry: + def __init__(self, name, num = None): + # if 'num' is None, then 'name' contains "name;num;..." + if num == None: + a = name.strip().split(';') + if len(a) < 2: + raise ValueError; + name = a[0] + num = a[1] + self.name = name + self.num = num + self.speed = None + + def is_speed(self): + return len(self.name) == 1 + + def is_deleted(self): + return self.name[:8] == '!deleted' + + def match_type(self, patn): + # patn might match: + # 1: start of name + # 2: start of word in name + # 3: somewhere in name + # 4: somewhere in num + p = patn.lower() + n = name.lower() + l = len(p) + if n[0:l] == p: + return 1 + if n.find(' '+p) >= 0: + return 2 + if n.find(p) >= 0: + return 3 + if self.num.find(p): + return 4 + return -1 + + def same_num(self, num): + l = len(num) + if l < 4: + # too short to be at all useful + return False + # 8 is enough to identify + if l > 8: + l = 8 + return len(self.num) >= l and self.num[-l:] == num[-l:] + + def __cmp__(self, other): + if self.speed and not other.speed: + return -1 + if other.speed and not self.speed: + return 1 + return cmp(self.name, other.name) + +class contacts: + def __init__(self): + try: + self.file = '/data/address-book' + self.load() + except: + self.file = '/home/neilb/home/mobile-numbers-jan-08' + self.load() + + def load(self): + self.list = [] + self.deleted = [] + self.speed = {} + speed = {} + f = open(self.file) + for l in f: + e = entry(l) + if e.is_speed(): + speed[e.num] = e.name + elif e.is_deleted(): + self.deleted.append(e) + else: + self.list.append(e) + if speed: + for i in range(len(self.list)): + if self.list[i].name in speed: + self.speed[speed[self.list[i].name]] = self.list[i] + self.list[i].speed = speed[self.list[i].name] + self.resort() + + def resort(self): + self.list.sort() + self.deleted.sort() + + def save(self): + f = open(self.file + '.new', 'w') + for e in self.list: + f.write(e.name+';'+e.num+';\n') + if e.speed: + f.write(e.speed+';'+e.name+';\n') + for e in self.deleted: + f.write(e.name+';'+e.num+';\n') + f.close() + os.rename(self.file+'.new', self.file) + + def add(self, name, num, speed = None): + c = entry(name, num) + self.list.append(c) + if speed: + c.speed = speed + self.speed[speed] = c + self.resort() + + def undelete(self, ind): + e = self.deleted[ind] + del self.deleted[ind] + s = e.name + if s[:8] == '!deleted': + p = s.find('-') + if p <= 0: + p = 7 + e.name = s[p+1:] + self.list.append(e) + self.resort() + if e.speed: + self.speed[e.speed] = e + + def delete(self, ind): + e = self.list[ind] + del self.list[ind] + n = time.strftime('!deleted.%Y.%m.%d-') + e.name + e.name = n + self.deleted.append(e) + self.resort() + + def find_num(self, num): + for e in self.list: + if e.same_num(num): + return e + return None + +if __name__ == "__main__": + c = contacts() + print c.speed.keys() diff --git a/contacts/contacts.py b/contacts/contacts.py new file mode 100644 index 0000000..5549848 --- /dev/null +++ b/contacts/contacts.py @@ -0,0 +1,619 @@ +#!/usr/bin/env python + +# +# Contacts manager +# Currently a 'contact' is a name, a number, and a speed-dial letter +# +# We have a content pane and a row of buttons at the bottom. +# The content is either: +# - list of contact names - highlighted minipane at bottom with number +# - detailed contact info that can be editted (name, number, speed-pos) +# +# When list of contacts are displayed, typing a char adds that char to +# a substring and only contacts containing that substring are listed. +# An extra entry at the start is given which matches exactly the substring +# and can be used to create a new entry. +# Alternately, the list can show only 'deleted' entries, still with substring +# matching. Colour is different in this case. +# +# Buttons for contact list are: +# When nothing selected (list_none): +# New ALL Undelete +# When contact selected (list_sel): +# Call SMS Open ALL +# When new-entry selected (list_new): +# Create ALL +# When viewing deleted entries and nothing or first is selected (del_none): +# Return +# When viewing deleted entries and one is selected (del_sel): +# Undelete Open Return +# +# When the detailed contact info is displayed all fields can be +# edited and change background colour if they differ from stored value +# Fields are: Name Number +# Button for details are: +# When nothing is changed (edit_nil): +# Call SMS Close Delete +# When something is changed on new entry (edit_new) +# Discard Create +# When something is changed on old entry (edit old) +# Restore Save Create +# +# 'delete' doesn't actually delete, but adds '!delete$DATE-' to the name which +# causes most lookups to ignore the entry. +# +# TODO +# - find way to properly reset 'current' pos after edit +# - have a global 'state' object which other things refer to +# It has an 'updated' state which other objects can connect to +# - save file after an update + +import gtk, pango, time, gobject, os +from scrawl import Scrawl +from listselect import ListSelect +from contactdb import contacts + +def strip_prefix(num): + if num[:2] == "02": + return num[2:] + if num[:2] == "04": + return num[1:] + if num[:4] == "+612": + return num[4:] + if num[:4] == "+614": + return num[3:] + return num + +def match_num(num, str): + """ 'num' is a phone number, and 'str' is a search string + We want to allow matches that ignore the country/local prefix + which might be "02" or "+612" or something else in other countries. + But I'm not in other countries, so just strip those if present. + """ + if num.find(str) >= 0: + return True + if "+612".find(str) == 0: + return True + if "02".find(str) == 0: + return True + return strip_prefix(num).find(strip_prefix(str)) == 0 + +class Contacts(gtk.Window): + def __init__(self): + gtk.Window.__init__(self) + self.set_default_size(480,640) + self.set_title("Contacts") + self.connect('destroy', self.close_win) + + self.current = None + self.timer = None + self.make_ui() + self.load_book() + self.show() + self.voice_cb = None + self.sms_cb = None + self.undeleting = False + self.watch_clip('contact-find') + + def make_ui(self): + # UI consists of: + # list of contacts -or- + # editable field + # -and- + # variable list of buttons. + # + ctx = self.get_pango_context() + fd = ctx.get_font_description() + fd.set_absolute_size(35*pango.SCALE) + self.fd = fd + + v = gtk.VBox(); v.show() + self.add(v) + + s = self.listui() + self.lst = s + v.pack_start(s, expand=True) + s.show() + self.show() + self.sc.set_colour('red') + + s = self.editui() + v.pack_start(s, expand = True) + s.hide() + self.ed = s + + bv = gtk.VBox(); bv.show(); v.pack_start(bv, expand=False) + def hide_some(w): + for c in w.get_children(): + c.hide() + bv.hide_some = lambda : hide_some(bv) + self.buttons = bv + + b = self.buttonlist(bv) + self.button(b, 'New', self.open) + self.button(b, 'Undelete', self.undelete) + self.button(b, 'ALL', self.all) + self.list_none = b + + b = self.buttonlist(bv) + self.button(b, 'Call', self.call) + self.button(b, 'SMS', self.sms) + self.button(b, 'Open', self.open) + self.button(b, 'Delete', self.delete) + self.list_sel = b + + b = self.buttonlist(bv) + self.button(b, 'Create', self.open) + self.button(b, 'ALL', self.all) + self.list_new = b + + b = self.buttonlist(bv) + self.button(b, 'Return', self.all) + self.del_none = b + + b = self.buttonlist(bv) + self.button(b, 'Undelete', self.delete) + self.button(b, 'Open', self.open) + self.button(b, 'Return', self.all) + self.del_sel = b + + b = self.buttonlist(bv) + self.button(b, 'Call', self.call) + self.button(b, 'SMS', self.sms) + self.button(b, 'Close', self.close) + self.button(b, 'Delete', self.delete) + self.edit_nil = b + + b = self.buttonlist(bv) + self.button(b, 'Discard', self.close) + self.button(b, 'Create', self.create) + self.edit_new = b + + b = self.buttonlist(bv) + self.button(b, 'Restore', self.open) + self.button(b, 'Save', self.save) + self.button(b, 'Create', self.create) + self.edit_old = b + + self.list_none.show() + + def listui(self): + s = ListSelect(markup=True); s.show() + s.set_format("normal","black", background="grey", selected="white") + s.set_format("deleted","red", background="grey", selected="white") + s.set_format("virtual","blue", background="grey", selected="white") + s.connect('selected', self.selected) + s.set_zoom(37) + self.clist = contact_list() + s.list = self.clist + s.list_changed() + self.sel = s + def gottap(p): + x,y = p + s.tap(x,y) + self.sc = Scrawl(s, self.gotsym, gottap, None, None) + + s.set_events(s.get_events() | gtk.gdk.KEY_PRESS_MASK) + def key(list, ev): + if len(ev.string) == 1: + self.gotsym(ev.string) + elif ev.keyval == 65288: + self.gotsym('') + else: + #print ev.keyval, len(ev.string), ev.string + pass + s.connect('key_press_event', key) + s.set_flags(gtk.CAN_FOCUS) + s.grab_focus() + + v = gtk.VBox(); v.show() + v.pack_start(s, expand=True) + l = gtk.Label('') + l.modify_font(self.fd) + self.number_label = l + v.pack_start(l, expand=False) + return v + + def editui(self): + ui = gtk.VBox() + + self.fields = {} + self.field(ui, 'Name') + self.field(ui, 'Number') + self.field(ui, 'Speed Number') + return ui + + def field(self, v, lbl): + h = gtk.HBox(); h.show() + l = gtk.Label(lbl); l.show() + l.modify_font(self.fd) + h.pack_start(l, expand=False) + e = gtk.Entry(); e.show() + e.modify_font(self.fd) + h.pack_start(e, expand=True) + e.connect('changed', self.field_update, lbl) + v.pack_start(h, expand=True, fill=True) + self.fields[lbl] = e + return e + + def buttonlist(self, v): + b = gtk.HBox() + b.set_homogeneous(True) + b.hide() + v.pack_end(b, expand=False) + return b + + def button(self, bl, label, func): + b = gtk.Button() + if label[0:3] == "gtk" : + img = gtk.image_new_from_stock(label, self.isize) + img.show() + b.add(img) + else: + b.set_label(label) + b.child.modify_font(self.fd) + b.show() + b.connect('clicked', func) + b.set_focus_on_click(False) + bl.pack_start(b, expand = True) + + def close_win(self, widget): + gtk.main_quit() + + def selected(self, list, item): + self.buttons.hide_some() + + if item == None: + item = -1 + if self.undeleting: + if item < 0 or (item == 0 and self.clist.str != ''): + self.del_none.show() + else: + self.del_sel.show() + self.current = self.clist.getitem(item) + elif item < 0: + self.list_none.show() + self.number_label.hide() + self.current = None + elif item == 0 and self.clist.str != '': + self.list_new.show() + self.number_label.hide() + self.current = None + else: + self.list_sel.show() + i = self.clist[item] + self.current = self.clist.getitem(item) + if i == None: + self.number_label.hide() + else: + self.number_label.set_text(self.clist.list[self.current].num) + self.number_label.show() + + def field_update(self, ev, fld): + if self.flds[fld] == self.fields[fld].get_text(): + self.fields[fld].modify_base(gtk.STATE_NORMAL,None) + else: + self.fields[fld].modify_base(gtk.STATE_NORMAL,gtk.gdk.color_parse('yellow')) + + same = True + for i in ['Name', 'Number', 'Speed Number']: + if self.fields[i].get_text() != self.flds[i]: + same = False + self.buttons.hide_some() + if self.current == None: + self.edit_new.show() + elif same: + self.edit_nil.show() + else: + self.edit_old.show() + pass + + def load_book(self): + self.book = contacts() + self.clist.set_list(self.book.list) + + def queue_save(self): + if self.timer: + gobject.source_remove(self.timer) + self.timer = gobject.timeout_add(15*1000, self.do_save) + def do_save(self): + if self.timer: + gobject.source_remove(self.timer) + self.timer = None + self.book.save() + + def gotsym(self,sym): + if sym == '': + s = self.clist.str[:-1] + elif len(sym) > 1: + s = self.clist.str + elif ord(sym) >= 32: + s = self.clist.str + sym + else: + return + self.clist.set_str(s) + self.sel.list_changed() + self.sel.select(None) + + def watch_clip(self, board): + self.cb = gtk.Clipboard(selection=board) + self.targets = [ (gtk.gdk.SELECTION_TYPE_STRING, 0, 0) ] + self.got_clip(self.cb, None) + self.cb.set_with_data(self.targets, + self.get_clip, self.got_clip, None) + + + def got_clip(self, clipb, data): + s = clipb.wait_for_text() + if not s: + print 'not s' + return + print 'got', s + self.clist.set_str(s) + self.lst.show() + self.sel.grab_focus() + self.ed.hide() + self.sel.list_changed() + self.sel.select(None) + self.cb.set_with_data(self.targets, self.get_clip, self.got_clip, None) + self.present() + + def get_clip(self, sel, seldata, info, data): + sel.set_text("Contact Please") + + def open(self, ev): + self.lst.hide() + self.ed.show() + + self.buttons.hide_some() + if self.current == None: + self.edit_new.show() + else: + self.edit_nil.show() + self.flds = {} + self.flds['Name'] = '' + self.flds['Number'] = '' + self.flds['Speed Number'] = '' + if self.current != None: + current = self.clist.list[self.current] + self.flds['Name'] = current.name + self.flds['Number'] = current.num + self.flds['Speed Number'] = current.speed + if current.speed == None: + self.flds['Speed Number'] = "" + elif self.clist.str: + num = True + for d in self.clist.str: + if not d.isdigit() and d != '+': + num = False + if num: + self.flds['Number'] = self.clist.str + else: + self.flds['Name'] = self.clist.str + + for f in ['Name', 'Number', 'Speed Number'] : + c = self.flds[f] + if not c: + c = "" + self.fields[f].set_text(c) + self.fields[f].modify_base(gtk.STATE_NORMAL,None) + + def all(self, ev): + self.clist.set_str('') + self.undeleting = False + self.current = None + self.clist.set_list(self.book.list) + self.sel.select(None) + self.sel.list_changed() + pass + def create(self, ev): + self.save(None) + def save(self,ev): + name = self.fields['Name'].get_text() + num = self.fields['Number'].get_text() + speeds = self.fields['Speed Number'].get_text() + speed = speeds + if speed == "": + speed = None + if name == '' or name.find(';') >= 0: + self.fields['Name'].modify_base(gtk.STATE_NORMAL,gtk.gdk.color_parse('pink')) + return + if num == '' or num.find(';') >= 0: + self.fields['Number'].modify_base(gtk.STATE_NORMAL,gtk.gdk.color_parse('pink')) + return + if len(speeds) > 1 or speeds.find(';') >= 0: + self.fields['Speed Number'].modify_base(gtk.STATE_NORMAL,gtk.gdk.color_parse('pink')) + return + if self.current == None or ev == None: + self.book.add(name, num, speed) + else: + current = self.book.list[self.current] + current.name = name + current.num = num + current.speed = speed + self.flds['Name'] = name + self.flds['Number'] = num + self.flds['Speed Number'] = speeds + self.book.resort() + self.clist.set_list(self.book.list) + self.sel.list_changed() + self.close(ev) + self.queue_save() + + def delete(self, ev): + if self.current != None: + if self.undeleting: + self.book.undelete(self.current) + self.clist.set_list(self.book.deleted) + else: + self.book.delete(self.current) + self.clist.set_list(self.book.list) + self.sel.list_changed() + self.close(ev) + self.queue_save() + pass + + def undelete(self, ev): + self.undeleting = True + self.clist.set_str('') + self.clist.set_list(self.book.deleted) + self.current = None + self.sel.list_changed() + self.sel.select(None) + pass + def call(self, ev): + if not self.voice_cb: + self.voice_cb = gtk.Clipboard(selection='voice-dial') + self.call_str = self.clist.list[self.current].num + self.voice_cb.set_with_data([ (gtk.gdk.SELECTION_TYPE_STRING, 0, 0) ], + self.call_getdata, self.call_cleardata, None) + def call_getdata(self, clipb, sel, info, data): + sel.set_text(self.call_str) + def call_cleardata(self, clipb, data): + self.call_str = "" + + def sms(self, ev): + if not self.sms_cb: + self.sms_cb = gtk.Clipboard(selection='sms-new') + self.sms_str = self.clist.list[self.current].num + self.sms_cb.set_with_data([ (gtk.gdk.SELECTION_TYPE_STRING, 0, 0)], + self.sms_get_data, self.sms_cleardata, None) + def sms_get_data(self, clipb, sel, info, data): + sel.set_text(self.sms_str) + def sms_cleardata(self, clipb, data): + self.sms_str = "" + + + def close(self, ev): + self.lst.show() + self.sel.grab_focus() + self.ed.hide() + if self.current != None and self.current >= len(self.clist): + self.current = None + self.selected(None, self.sel.selected) + +class contact_list: + def __init__(self): + self.set_list([]) + def set_list(self, list): + self.list = list + self.match_start = [] + self.match_word = [] + self.match_any = [] + self.match_str = '' + self.total = 0 + self.str = '' + + def set_str(self,str): + self.str = str + + def recalc(self): + if self.str == self.match_str: + return + if self.match_str != '' and self.str[:len(self.match_str)] == self.match_str: + # str is a bit longer + self.recalc_quick() + return + self.match_start = [] + self.match_word = [] + self.match_any = [] + self.match_str = self.str + s = self.str.lower() + l = len(self.str) + for i in range(len(self.list)): + name = self.list[i].name.lower() + if name[0:l] == s: + self.match_start.append(i) + elif name.find(' '+s) >= 0: + self.match_word.append(i) + elif name.find(s) >= 0 or match_num(self.list[i].num, s): + self.match_any.append(i) + self.total = len(self.match_start) + len(self.match_word) + len(self.match_any) + + def recalc_quick(self): + # The string has been extended so we only look through what we already have + self.match_str = self.str + s = self.str.lower() + l = len(self.str) + + lst = self.match_start + self.match_start = [] + for i in lst: + name = self.list[i].name.lower() + if name[0:l] == s: + self.match_start.append(i) + else: + self.match_word.append(i) + self.match_word.sort() + lst = self.match_word + self.match_word = [] + for i in lst: + name = self.list[i].name.lower() + if name.find(' '+s) >= 0: + self.match_word.append(i) + else: + self.match_any.append(i) + self.match_any.sort() + lst = self.match_any + self.match_any = [] + for i in lst: + name = self.list[i].name.lower() + if name.find(s) >= 0 or match_num(self.list[i].num, s): + self.match_any.append(i) + self.total = len(self.match_start) + len(self.match_word) + len(self.match_any) + + def __len__(self): + self.recalc() + if self.str == '': + return len(self.list) + else: + return self.total + 1 + def getitem(self, ind): + if ind < 0: + print '!!!!', ind + if self.str == '': + return ind + + if ind == 0: + return -1 + ind -= 1 + if ind < len(self.match_start): + return self.match_start[ind] + ind -= len(self.match_start) + if ind < len(self.match_word): + return self.match_word[ind] + ind -= len(self.match_word) + if ind < len(self.match_any): + return self.match_any[ind] + return None + + def __getitem__(self, ind): + + i = self.getitem(ind) + if i < 0: + return (protect(self.str), 'virtual') + if i == None: + return None + s = self.list[i].name + n = self.list[i].num + s = protect(s) + n = protect(n) + if s[:8] == '!deleted': + p = s.find('-') + if p <= 0: + p = 7 + return (s[p+1:],'deleted') + else: + return (s + '\n '+ n + '', 'normal') + +def protect(txt): + txt = txt.replace('&', '&') + txt = txt.replace('<', '<') + txt = txt.replace('>', '>') + return txt + +if __name__ == "__main__": + + c = Contacts() + gtk.main() + diff --git a/gps-utils/gpstime b/gps-utils/gpstime new file mode 100755 index 0000000..c34291d --- /dev/null +++ b/gps-utils/gpstime @@ -0,0 +1,33 @@ +#!/bin/bash + + +gpspipe -r -n 30 | { +sum=0 +cnt=0 +while IFS=, read x t d m y r; + do [ $x = '$GPZDA' ] || continue + [ -z "$t" ] && continue + now=`date +%s` + hr=${t:0:2} + mn=${t:2:2} + sc=${t:4:2} + + then=`date --utc --date="$y-$m-$d $hr:$mn:$sc" +%s` + sum=$[sum+then-now] + cnt=$[cnt+1] +done +if [ $cnt -gt 0 ] +then + diff=$[sum/cnt] + if [ $diff -lt -4 -o $diff -gt 4 ] + then echo "Change by $diff seconds" + date --set "+$diff seconds" + hwclock -w + else + echo "Diff is $diff - no change" + fi +else + echo "No time found" +fi +} + diff --git a/gps-utils/gpstz b/gps-utils/gpstz new file mode 100755 index 0000000..be7a405 --- /dev/null +++ b/gps-utils/gpstz @@ -0,0 +1,62 @@ +#!/bin/bash + +case $1 in + */* ) + if cmp -s /etc/localtime /usr/share/zoneinfo/$1 + then : localtime is OK + else : echo Copying to localtime + cp /usr/share/zoneinfo/$1 /etc/localtime + fi + if [ `cat /etc/timezone` != $1 ] + then : echo Setting /etc/timezone + echo $1 > /etc/timezone + fi + exit 0 + ;; + --list ) ;; + * ) echo >&2 Usage: gpstz [--list] zone/name + exit 1 +esac + +gpspipe -r -n 20 | grep GPGGA | while IFS=, read a tm lat NS long EW etc + do + long=${long%.*} lat=${lat%.*} + case $NS in + N) lat=+$lat;; + S) lat=-$lat;; + esac + case $EW in + E) long=+$long ;; + W) long=-$long ;; + esac + # echo $lat $long + mind=9999999999 + while read country loc tz desc + do + case $country in + \#* ) continue;; + esac + case $loc in + [-+][0-9][0-9][0-9][0-9][-+][0-9][0-9][0-9][0-9][0-9] ) + tlat=${loc%??????} + tlat=${tlat#?} + tlat=${tlat#0} + tlat=${tlat#0} + tlat=${tlat#0} + tlat=${loc%??????????}$tlat + tlong=${loc#?????} + slong=${tlong%?????} + tlong=${tlong#?} + tlong=${tlong#0} + tlong=${tlong#0} + tlong=$slong${tlong#0} + ;; + * ) continue + esac + # echo $tz at $tlat $tlong + x=$[long-tlong] y=$[lat-tlat] + d=$[x*x+y*y] + echo $d $tz + done < /usr/share/zoneinfo/zone.tab + break + done | sort -n | sed 10q diff --git a/gps-utils/ubxgen b/gps-utils/ubxgen new file mode 100755 index 0000000..a4efe2d --- /dev/null +++ b/gps-utils/ubxgen @@ -0,0 +1,68 @@ +#!/usr/bin/python +# +# ubx packet generator +# +# v0.2 +# +# Wilfried Klaebe +# NeilBrown +# +# Usage: +# +# ubxgen.py 06 13 04 00 01 00 00 00 > packet.ubx +# +# prepends 0xb5 0x62 header, +# appends checksum, +# outputs binary packet to stdout +# +# Numbers can be given in decimal with a suffix 'dN' where +# 'N' is the number of bytes. These are converted in little-endian +# The value 'L' can be given which is th 2-byte length +# of the rest of the message +# +# you can send the packet to GPS chip like this: +# +# cat packet.ubx > /dev/ttySAC1 + +import sys +import binascii + +cs0=0 +cs1=0 + +sys.stdout.write("\xb5\x62") + +outbuf = [] +leng = None + +for d in sys.argv[1:]: + if d == 'L': + leng = len(outbuf) + outbuf.append(0) + outbuf.append(0) + elif 'd' in d: + p = d.index('d') + bytes = int(d[p+1:]) + d = int(d[0:p]) + while bytes > 0: + b = d % 256 + d = int(d/256) + outbuf.append(b) + bytes -= 1 + else: + c = binascii.unhexlify(d) + outbuf.append(ord(c)) + +if leng != None: + l = len(outbuf) - (leng + 2) + outbuf[leng] = l % 256 + outbuf[leng+1] = int(l/256) + +for c in outbuf: + sys.stdout.write(chr(c)) + cs0 += c + cs0 &= 255 + cs1 += cs0 + cs1 &= 255 + +sys.stdout.write(chr(cs0)+chr(cs1)) diff --git a/gsm/BUG b/gsm/BUG new file mode 100644 index 0000000..8154013 --- /dev/null +++ b/gsm/BUG @@ -0,0 +1,18 @@ +Thu Aug 30 22:30:11 2012 state becomes flight +Thu Aug 30 22:30:11 2012 check flightmode got 1 +Thu Aug 30 22:30:11 2012 advance flight chooses 0, 0 +Thu Aug 30 22:30:11 2012 send AT command +CFUN=0 2000 +Traceback (most recent call last): + File "/usr/local/bin/gsmd", line 859, in + a = GsmD('/dev/ttyHS_Application') + File "/usr/local/bin/gsmd", line 648, in __init__ + self.advance() + File "/usr/local/bin/gsmd", line 746, in advance + control[self.gstate][t].start(self) + File "/usr/local/bin/gsmd", line 106, in start + self.advance(channel) + File "/usr/local/bin/gsmd", line 170, in advance + channel.atcmd(at) + File "/usr/local/bin/atchan.py", line 114, in atcmd + self.sock.write('AT' + cmd + '\r') +AttributeError: 'NoneType' object has no attribute 'write' diff --git a/gsm/TOFIX b/gsm/TOFIX new file mode 100644 index 0000000..bff8aba --- /dev/null +++ b/gsm/TOFIX @@ -0,0 +1,36 @@ +Sun Dec 23 10:36:08 2012 send AT command +CPAS 2000 +Sun Dec 23 10:36:08 2012 received AT response +CPAS: 4 +Sun Dec 23 10:36:08 2012 call_status got +CPAS: 4 +Sun Dec 23 10:36:08 2012 s = 4 +Sun Dec 23 10:36:08 2012 received AT response OK +Sun Dec 23 10:36:08 2012 call_status got OK +Sun Dec 23 10:36:08 2012 advance on-call chooses 0, 2000 +Sun Dec 23 10:36:08 2012 Sleeping for 2.000000 seconds +Sun Dec 23 10:36:08 2012 received AT response OK +Sun Dec 23 10:36:08 2012 received AT response OK +Sun Dec 23 10:36:08 2012 received AT response +CPAS: 4 +Sun Dec 23 10:36:08 2012 received AT response OK +Sun Dec 23 10:36:09 2012 Check call got 0415836820 +Sun Dec 23 10:36:10 2012 Timer Fired +Sun Dec 23 10:36:10 2012 advance on-call chooses 0, 5 +Sun Dec 23 10:36:10 2012 Sleeping for 0.005000 seconds +Sun Dec 23 10:36:10 2012 Timer Fired +Sun Dec 23 10:36:10 2012 advance on-call chooses 0, 5 +Sun Dec 23 10:36:10 2012 Sleeping for 0.005000 seconds +Sun Dec 23 10:36:10 2012 Timer Fired +Sun Dec 23 10:36:10 2012 advance on-call chooses 0, 4 +Sun Dec 23 10:36:10 2012 Sleeping for 0.004000 seconds +Sun Dec 23 10:36:10 2012 Timer Fired +Sun Dec 23 10:36:10 2012 advance on-call chooses 2, 0 +Sun Dec 23 10:36:10 2012 send AT command +CPAS 2000 +Sun Dec 23 10:36:10 2012 Timer Fired +Sun Dec 23 10:36:10 2012 send AT command 2000 +Sun Dec 23 10:36:10 2012 Timer Fired +Sun Dec 23 10:36:10 2012 send AT command 2000 +Sun Dec 23 10:36:10 2012 Timer Fired +Sun Dec 23 10:36:10 2012 send AT command 2000 +Sun Dec 23 10:36:10 2012 received AT response +CPAS: 4 +Sun Dec 23 10:36:10 2012 received AT response OK +Sun Dec 23 10:36:10 2012 call_status got OK +Sun Dec 23 10:36:10 2012 send AT command +CPAS 2000 +: diff --git a/gsm/atchan.py b/gsm/atchan.py new file mode 100644 index 0000000..bc92b23 --- /dev/null +++ b/gsm/atchan.py @@ -0,0 +1,208 @@ + +# +# Handle a connection to an AT device via /dev/ttyXX +# +# We directly support high level commands (reset_modem +# etc) but don't know anything about AT commands - we just send them +# through and hand back reply. Replies also go via a callback +# We also provide timeout support, but someone else needs to tell us +# when to set a timeout, and when to clear it. +# +# This is usually subclassed by code with an agenda. + +import gobject, sys, os, time +import termios +from tracing import log +from socket import * + +class AtChannel: + def __init__(self, path): + self.path = path + self.connected = False + self.watcher = None + self.sock = None + self.buf = "" + self.linelist = [] + + self.pending = False + self.timer = None + + def disconnect(self): + if self.watcher: + gobject.source_remove(self.watcher) + self.watcher = None + if self.sock: + self.sock.close() + self.sock = None + self.connected = False + + def connect(self): + if self.sock != None: + return + log("connect to", self.path) + s = open(self.path,"r+") + #s.setblocking(0) + fd = s.fileno() + attr = termios.tcgetattr(fd) + attr[3] = attr[3] & ~termios.ICANON & ~termios.ECHO & ~termios.ISIG + attr[6][termios.VMIN] = 0 + attr[6][termios.VTIME] = 0 + termios.tcsetattr(fd, termios.TCSANOW, attr) + + self.watcher = gobject.io_add_watch(s, gobject.IO_IN, self.readdata) + self.sock = s + self.connected = True + + def readdata(self, io, arg): + try: + r = self.sock.read(1000) + except IOError: + # no data there really. + return True + if not r: + # pipe closed + if io != None: + self.getline(None) + return False + r = self.buf + r + ra = r.split('\n') + self.buf = ra[-1]; + del ra[-1] + for ln in ra: + ln = ln.strip('\r') + self.getline(ln) + # FIXME this should be configurable + if self.buf == '> ': + self.getline(self.buf) + self.buf = '' + return True + + def getline(self, line): + if line == None: + log("received EOF") + self.takeline(line) + if self.pending: + self.pending = False + gobject.source_remove(self.timer) + self.timer = None + return + if len(line): + log("received AT response", line) + if self.takeline(line): + if self.pending: + self.pending = False + gobject.source_remove(self.timer) + self.timer = None + + def atcmd(self, cmd, timeout = 2000): + """ + Send the command, preceeded by 'AT' and set a timeout. + self.takeline() should return True when the command + has been responded to, otherwise we will call + self.timedout() after the time. + """ + self.set_timeout(timeout) + log("send AT command", cmd, timeout) + try: + self.sock.write('AT' + cmd + '\r') + self.sock.flush() + except IOError: + self.cancel_timeout() + self.set_timeout(10) + + def timer_fired(self): + log("Timer Fired") + self.pending = False + self.timer = None + self.timedout() + return False + + def set_timeout(self, delay): + if self.pending: + raise ValueError + self.timer = gobject.timeout_add(delay, self.timer_fired) + self.pending = True + + def cancel_timeout(self): + if self.pending: + gobject.source_remove(self.timer) + self.pending = False + + def abort_timeout(self): + if self.pending: + self.cancel_timeout() + self.set_timeout(0) + + # these are likely to be over-ridden by a child class + def takeline(self, line): + self.linelist.append(line) + + def wait_line(self, timeout): + self.cancel_timeout() + self.set_timeout(timeout) + if len(self.linelist) == 0: + self.readdata(None, None) + c = gobject.main_context_default() + while not self.linelist and self.pending: + c.iteration() + if self.linelist: + self.cancel_timeout() + l = self.linelist[0] + del self.linelist[0] + return l + else: + return None + def timedout(self): + pass + + + def chat(self, mesg, resp, timeout = 1000): + """ + Send the message (if not 'None') and wait up to + 'timeout' for one of the responses (regexp) + Return None on timeout, or number of response. + combined with an array of the messages received. + """ + if mesg: + log("send command", mesg) + try: + self.sock.write(mesg + '\r\n') + self.sock.flush() + except error: + timeout = 10 + + conv = [] + while True: + l = self.wait_line(timeout) + if l == None: + return (None, conv) + conv.append(l) + for i in range(len(resp)): + ptn = resp[i] + if type(ptn) == str: + if ptn == l.strip(): + return (i, conv) + else: + if resp[i].match(l): + return (i, conv) + + def chat1(self, mesg, resp, timeout=1000): + n,c = self.chat(mesg, resp, timeout = timeout) + return n + + +def found(list, patn): + """ + see if patn can be found in the list of strings + """ + for l in list: + l = l.strip() + if type(patn) == str: + if l == patn: + return True + else: + p = patn.match(l) + if p: + return p + return False + diff --git a/gsm/gsm-carriers.py b/gsm/gsm-carriers.py new file mode 100755 index 0000000..ff54da8 --- /dev/null +++ b/gsm/gsm-carriers.py @@ -0,0 +1,31 @@ +#!/usr/bin/env python + +# get list of currently available carriers from Option 3G +# +from atchan import AtChannel, found +import sys, re + +chan = AtChannel(path='/dev/ttyHS_Control') +chan.connect() + +if chan.chat1('ATE0', ['OK','ERROR']) != 0: + sys.exit(1) + +n,c = chan.chat('AT+COPS=?', ['OK','ERROR'], timeout=45000) +if n == None and len(c) == 0: + # need to poke to get a response + n,c = chan.chat('', ['OK','ERROR'], timeout=10000) +if n == None and len(c) == 0: + # need to poke to get a response + n,c = chan.chat('', ['OK','ERROR'], timeout=10000) + +if n != 0: + sys.exit(1) + + +m = found(c, re.compile('^\+COPS: (.*)')) +if m: + clist = re.findall('\((\d+),"([^"]*)","([^"]*)","([^"]*)",(\d+)\)', m.group(1)) + for (mode, long, short, num, type) in clist: + print num, type, '"%s" "%s"' % (short, long) + diff --git a/gsm/gsm-data.py b/gsm/gsm-data.py new file mode 100644 index 0000000..5816591 --- /dev/null +++ b/gsm/gsm-data.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python + +# establish data connection on Option 3G module. +# +# AT+CGDCONT=1,"IP","${APN}" +# OK +# AT_OWANCALL=1,1,1 +# OK +# _OWANCALL: 1, 1 +# AT_OWANDATA? +# _OWANDATA: 1, 10.6.177.182, 0.0.0.0, 211.29.132.12, 61.88.88.88, 0.0.0.0, 0.0.0.0,144000 +# ifconfig hso0 10.6.177.182 +# route add default dev hso0 +# echo nameserver 211.29.132.12 > /etc/resolv.conf +# echo nameserver 61.88.88.88 >> /etc/resolv.conf +# +# ...but.... +# we might already be connected, and we might not know, and just +# stopping/starting doesn't seem to do much - no notification. +# so: +# - first check status. If up, configure and done +# - if not, enable and wait. +# - on timeout, check status again. + +#APN="exetel1" +APN="INTERNET" +import atchan, sys, re, os + +def check_status(chan): + n,c = chan.chat('AT_OWANDATA?', ['OK','ERROR']) + want = re.compile('_OWANDATA: 1, ([0-9.]+), [0-9.]+, ([0-9.]+), ([0-9.]+), [0-9.]+, [0-9.]+,\d+$') + if n == 0: + m = atchan.found(c, want) + if n != 0 or not m: + return False + return m + +def configure(m): + g = m.groups() + ip = g[0] + ns1= g[1] + ns2= g[2] + print ip, ns1, ns2 + os.system("/sbin/ifconfig hso0 %s" % ip) + os.system('route add default dev hso0') + f = open("/etc/resolv.conf", "w") + f.write("nameserver %s\n" % ns1) + f.write("nameserver %s\n" % ns2) + f.close() + +def disconnect(chan): + n,c = chan.chat('AT_OWANCALL=1,0,0', ['OK','ERROR']) + +chan = atchan.AtChannel(path="/dev/ttyHS_Control") +chan.connect() + +chan.chat1('ATE0', ['OK','ERROR']) +if chan.chat1('AT+CGDCONT=1,"IP","%s"' % APN, ['OK','ERROR']) != 0: + print 'Could not set APN' + sys.exit(1) + +m = check_status(chan) + +if sys.argv[1] == 'status': + if m: + print "Active: ", m.groups()[0] + else: + print "inactive" + sys.exit(0) +if sys.argv[1] == "off": + if m: + disconnect(chan) + else: + print 'DATA already disconnected' + os.system('route delete default dev hso0') + os.system('/sbin/ifconfig hso0 down') + sys.exit(0) + +if m: + print 'already active' + configure(m) + sys.exit(0) + +want = re.compile('^_OWANCALL: *\d+, *(\d*)$') + +if chan.chat1('AT_OWANCALL=1,1,1', ['OK','ERROR']) != 0: + print 'Could not start data connection' + sys.exit(1) +l = chan.wait_line(10000) +if l == None: + print 'No response for DATA connect' +else: + m = want.match(l) + if m and m.groups()[0] == '1': + print 'Connected' + else: + print 'Connect failed' + sys.exit(1) + +m = check_status(chan) +if m: + configure(m) +else: + print 'Sorry, could not connect' + #disconnect(chan) diff --git a/gsm/gsm-getsms.py b/gsm/gsm-getsms.py new file mode 100644 index 0000000..01eaebc --- /dev/null +++ b/gsm/gsm-getsms.py @@ -0,0 +1,412 @@ +#!/usr/bin/env python +# coding=UTF-8 + +# Collect SMS messages from the GSM device. +# We store a list of messages that are thought to be +# in the SIM card: number from date +# e.g. 17 61403xxxxxx 09/02/17,20:28:36+44 +# As we read messages, if we find one that is not in that list, +# we record it in the SMS store, then update the list +# +# An option can specify either 'new' or 'all. +# 'new' will only ask for 'REC UNREAD' and so will be faster and so +# is appropriate when we know that a new message has arrived. +# 'all' reads all messages and so is appropriate for an occasional +# 'sync' like when first turning the phone on. +# +# If we discover that the SMS card is more than half full, we +# deleted the oldest messages. +# We discover this by 'all' finding lots of messages, or 'new' +# finding a message with a high index. +# For now, we "know" that the SIM card can hold 30 messages. +# +# We need to be careful about long messages. A multi-part message +# looks like e.g. +#+CMGL: 19,"REC UNREAD","61403xxxxxx",,"09/02/18,10:51:46+44",145,140 +#0500031C0201A8E8F41C949E83C2207B599E07B1DFEE33A85D9ECFC3E732888E0ED34165FCB85C26CF41747419344FBBCFEC32A85D9ECFC3E732889D6EA7E9A0F91B444787E9A024681C7683E6E532E88E0ED341E939485E1E97D3F6321914A683E8E832E84D4797E5A0B29B0C7ABB41ED3CC8282F9741F2BADB5D96BB40D7329B0D9AD3D36C36887E2FBBE9 +#+CMGL: 20,"REC UNREAD","61403xxxxxx",,"09/02/18,10:51:47+44",145,30 +#0500031C0202F2A0B71C347F83D8653ABD2C9F83E86FD0F90D72B95C2E17 + +# what about: +# 0281F001000081000019C9531BF4AED3E769721944479741F4F7DD0D4287D96C +# 02 81F0 ?? +# 010000810000 +# 19 (25 bytes) +# C9531BF4AED3E769721944479741F4F7DD0D4287D96C +# I'm outside the town hall +# This is a saved message. +# +# If that was just hex you could use +# perl -e 'print pack("H*","050....")' +# to print it.. but no... +# Looks like it decodes as: +# 05 - length of header, not including this byte +# 00 - concatentated SMS with 8 bit ref number (08 means 16 bit ref number) +# 03 - length of rest of header +# 1C - ref number for this concat-SMS +# 02 - number of parts in this SMS +# 01 - number of this part - counting starts from 1 +# A8E8F41C949E83C22.... message, 7 bits per char. so: +# A8 - 54 *2 + 0 54 == T 1010100 0 1010100 +# 0E8 - 68 *2 + 0 68 == h 1 1101000 1101000 +# F4 - 69 == i 11 110100 1101001 1 +# 1C 73 == s 000 11100 1110011 11 +# 94 20 == space 1001 0100 0100000 000 +# 9E 69 == i 10011 110 1101001 1001 +# 83 73 == s 100000 11 1110011 10011 +# 20 == space 0100000 0100000 + +# 153 characters in first message. 19*8 + 1 +# that uses 19*7+1 == 134 octets +# There are 6 in the header so a total of 140 +# second message has 27 letters - 3*8+3 +# That requires 3*7+3 == 24 octets. 30 with the 6 octet header. + +# then there are VCARD messages that look lie e.g. +#+CMGL: 2,"REC READ","61403xxxxxx",,"09/01/29,13:01:26+44",145,137 +#06050423F40000424547494E3A56434152440D0A56455253494F4E3A322E310D0A4E3A....0D0A454E443A56434152440D0A +#which is +#06050423F40000 +#then +#BEGIN:VCARD +#VERSION:2.1 +#N: ... +#... +#END:VCARD +# The 06050423f40000 +# might decode like: +# 06 - length of rest of header +# 05 - magic code meaning 'user data' +# 04 - length of rest of header... +# 23 - +# f4 - destination port '23f4' means 'vcard' +# 00 - +# 00 - 0000 is the origin port. +# +#in hex/ascii +# +# For now, ignore anything longer than the specified length. + +import os +import suspend +#os.environ['PYTRACE'] = '1' + +import atchan, sys, re +from storesms import SMSmesg, SMSstore + + +def load_mirror(filename): + # load an array of index address date + # from the file and store in a hash + rv = {} + try: + f = file(filename) + except IOError: + return rv + l = f.readline() + while l: + fields = l.strip().split(None, 1) + rv[fields[0]] = fields[1] + l = f.readline() + return rv + +def save_mirror(filename, hash): + n = filename + '.new' + f = open(n, 'w') + for i in hash: + f.write(i + ' ' + hash[i] + '\n') + f.close() + os.rename(n, filename) + +# GSM uses a 7-bit code that is not the same as ASCII... +# -*- coding: utf8 -*- +gsm = (u"@£$¥èéùìòÇ\nØø\rÅåΔ_ΦΓΛΩΠΨΣΘΞ\x1bÆæßÉ !\"#¤%&'()*+,-./0123456789:;<=>?" + u"¡ABCDEFGHIJKLMNOPQRSTUVWXYZÄÖÑÜ`¿abcdefghijklmnopqrstuvwxyzäöñüà") +ext = (u"````````````````````^```````````````````{}`````\\````````````[~]`" + u"|````````````````````````````````````€``````````````````````````") + +# Take a unicode string and produce a byte string for GSM +def gsm_encode(plaintext): + res = "" + for c in plaintext: + idx = gsm.find(c); + if idx != -1: + res += chr(idx) + continue + idx = ext.find(c) + if idx != -1: + res += chr(27) + res += chr(idx) + return res +# take a GSM byte string (7-in-8 conversion already done) and produce unicode +def gsm_decode(code): + uni = u'' + esc = False + for c in code: + n = ord(c) + if esc: + uni += ext[n] + esc = False + elif n == 27: + esc = True + else: + uni += gsm[n] + return uni + + +def sms_decode(msg,pos = 1): + #msg is a 7-in-8 encoding of a longer message. + carry = 0 + str = '' + while msg: + c = msg[0:2] + msg = msg[2:] + b = int(c, 16) + if pos == 0: + if carry: + str += chr(carry + (b&1)*64) + carry = 0 + b /= 2 + else: + b = (b << (pos-1)) | carry + carry = (b & 0xff80) >> 7 + b &= 0x7f + if (b & 0x7f) != 0: + str += chr(b&0x7f) + pos = (pos+1) % 7 + return gsm_decode(str) + +def sms_unicode_decode(msg): + # 2bytes unicode numbers - 4 syms each + m = u'' + for i in range(len(msg)/4): + c = int(msg[i*4:i*4+4],16) + m += unichr(c) + return m + +def cvt_telnum(type, msg, len): + if type == '81' or type == '91': + n = '' + for i in range(len): + n += msg[i + 1 - (i%2)*2] + if type == '91': + return '+' + n + else: + return n + if type == 'D0': + return sms_decode(msg) + return "?" + type + msg + +def cvt_date(msg): + #YYMMDDHHMMSSZZ -> 20YY/MM/DD HH:MM:SS+ZZ swapping letters + sep='0//,::+' + dt = '2' + for i in range(len(msg)/2): + dt += sep[i] + msg[i*2+1] + msg[i*2] + return dt + + +def main(): + mode = 'all' + for a in sys.argv[1:]: + if a == '-n': + mode = 'new' + else: + print "Unknown option:", a + sys.exit(1) + + pth = None + for p in ['/data','/media/card','/var/tmp']: + if os.path.exists(os.path.join(p,'SMS')): + pth = p + break + + if not pth: + print "Cannot find SMS directory" + sys.exit(1) + + dir = os.path.join(pth, 'SMS') + print "Using ", dir + store = SMSstore(dir) + + chan = atchan.AtChannel(path = '/dev/ttyHS_Control') + chan.connect() + + # consume any extra 'OK' that might be present + chan.chat1('', ['OK', 'ERROR']); + if chan.chat1('ATE0', ['OK', 'ERROR']) != 0: + sys.exit(1) + + # get ID of SIM card + n,c = chan.chat('AT+CIMI', ['OK', 'ERROR']) + CIMI='unknown' + for l in c: + l = l.strip() + if re.match('^\d+$', l): + CIMI = l + + mfile = os.path.join(dir, '.sim-mirror-'+CIMI) + #FIXME lock mirror file + mirror = load_mirror(mfile) + + chan.chat('AT+CMGF=0', ['OK','ERROR']) + if mode == 'new': + chan.atcmd('+CMGL=0') + else: + chan.atcmd('+CMGL=4') + + # reading the msg list might fail for some reason, so + # we always prime the mirror list with the previous version + # and only replace things, never assume they aren't there + # because we cannot see them + newmirror = mirror + mirror_seen = {} + + l = '' + state = 'waiting' + msg = '' + # text format + # indx state from name date type len + #+CMGL: 40,"REC READ","+61406022084",,"12/03/14,18:00:40+44" + # PDU MODE + #+CMGL: index, 0-4, unread read unsent sent all, ?? , byte len after header + #+CMGL: 3,1,,40 + #07911614786007F0 040B911654999946F100002120217035534417CE729ACD02BDD7203A3AEC5ECF5D2062D9ED4ECF01 + # 61450000641F000012021207533544 + # 07 is length (octets) of header (911614786007F0) + # 91 is SMSC number type; 81 is local? 91 is international D0 is textual D0456C915A6402 == EXETEL + # 1614786007F0 is the SMSC number: 61418706700 + # + # 04 is "SMS-DELIVER" and some other details 44 == ?? 24?? + # 0B is length of sende number (11) + # 91 is type as above + # 1654999946F1 is number: 61459999641 (F is padding) + # 00 is protocol id - always 0 ?? + # 00 is Data coding scheme. 00 is 7-bit default + # 21202170355344 is stime stamp: 12/02/12 07:35:53+44 + # 17 is length of body (23) + # CE729ACD02BDD7203A3AEC5ECF5D2062D9ED4ECF01 is 7-in-8 message + # + # other coding schemes: + # 08 is 16 bit unicode + # 11 is VCARD: 06050400E20080 C2E231E9D459874129B1A170EA886176B9A1A016993A182D26B3E164B919AEA1283A893AEB3028253614 + # looks like 7-in-8 with a 7/8 header + # or can be just a message. + # or ??? (message from AUST POST) + # 01 is much like 00 ?? + + + want = re.compile('^\+CMGL: (\d+),(\d+),("[^"]*")?,(\d+)$') + + found_one = False + while state == 'reading' or not (l[0:2] == 'OK' or l[0:5] == 'ERROR' or + l[0:10] == '+CMS ERROR'): + l = chan.wait_line(1000) + if l == None: + sys.exit(1) + print "Got (",state,")", l + if state == 'reading': + msg = l + if designation != '0' and designation != '1': + #send, not recv + state = 'waiting' + continue + if len(msg) >= msg_len: + state = 'waiting' + hlen = int(msg[0:2], 16) + hdr = msg[2:(2+hlen*2)] + # hdr is the sending number - don't care + msg = msg[2+hlen*2:] + # typ == 04 - SMS-DELIVER + typ = int(msg[0:2],16) + nlen = int(msg[2:4], 16) + ntype = msg[4:6] + numlen = (nlen + nlen % 2) + sender = cvt_telnum(ntype, msg[6:6+numlen], nlen) + msg = msg[6+numlen:] + proto = msg[0:2] + coding = msg[2:4] + date = cvt_date(msg[4:18]) + bdy_len = int(msg[18:20], 16) + body = msg[20:] + ref = None; part = None + + if body[0:6] == '050003': + # concatenated message with 8bit ref number + ref = body[6:8] + part = ( int(body[10:12],16), int(body[8:10], 16)) + if coding == '08': + txt = sms_unicode_decode(body) + else: + txt = sms_decode(body[12:],0) + elif body[0:6] == '060504': + # VCARD ?? + txt = sms_decode(body[14:]) + elif coding == '00': + txt = sms_decode(body) + elif coding == '11': + txt = sms_decode(body) + elif coding == '01': + txt = sms_decode(body) + elif coding == '08': + txt = sms_unicode_decode(body) + else: + print "ignoring", index, sender, date, body + continue + if ref == None: + print "found", index, sender, date, txt.encode('utf-8') + else: + print "found", index, ref, part, sender, date, repr(txt) + + if index in mirror and mirror[index] == date[2:] + ' ' + sender: + print "Already have that one" + else: + sms = SMSmesg(source='GSM', time=date[2:], sender=sender, + text = txt.encode('utf-8'), state = 'NEW', + ref= ref, part = part) + store.store(sms) + found_one = True + newmirror[index] = date[2:] + ' ' + sender + mirror_seen[index] = date[2:] + ' ' + sender + else: + m = want.match(l) + if m: + g = m.groups() + index = g[0] + designation = g[1] + msg_len = int(g[3], 10) + msg = '' + state = 'reading' + + mirror = newmirror + + if len(mirror) > 10: + rev = {} + dlist = [] + for i in mirror: + rev[mirror[i]+' '+str(i)] = i + dlist.append(mirror[i]+' '+str(i)) + dlist.sort() + for i in range(len(mirror) - 10): + dt=dlist[i] + ind = rev[dt] + print 'del', i, dt, ind + resp = chan.chat1('AT+CMGD=%s' % ind, ['OK', 'ERROR', '+CMS ERROR'], + timeout=3000) + if resp == 0 or ind not in mirror_seen: + del mirror[ind] + + save_mirror(mfile, mirror) + if found_one: + try: + f = open("/var/run/alert/sms", "w") + f.write("new") + f.close() + suspend.abort_cycle() + except: + pass + if mode == 'new' and not found_one: + sys.exit(1) + sys.exit(0) + +main() diff --git a/gsm/gsm-sms.py b/gsm/gsm-sms.py new file mode 100644 index 0000000..d410d92 --- /dev/null +++ b/gsm/gsm-sms.py @@ -0,0 +1,186 @@ +#!/usr/bin/env python + +# Send an SMS message using GSM. +# Args are: +# sender - ignored +# recipient - phone number +# message - no newlines +# +# We simply connect to the GSM module, +# check for a registration +# and +# AT+CMGS="recipient" +# > message +# > control-Z +# +# Sending multipart sms messages: +# ref: http://www.developershome.com/sms/cmgsCommand4.asp +# 1/ set PDU mode with AT+CMGF=0 +# 2/ split message into 153-char bundles +# 3/ create messages as follows: +# +# 00 - this says we aren't providing an SMSC number +# 41 - TPDU header - type is SMS-SUBMIT, user-data header present +# 00 - please assign a message reference number +# xx - length in digits of phone number +# 91 for IDD, 81 for "don't know what sort of number this is" +# 164030... BCD phone number, nibble-swapped, pad with F at end if needed +# 00 - protocol identifier?? +# 00 - encoding - 7 bit ascii +# XX - length of message in septets +# Then the message which starts with 7 septets of header that looks like 6 octets. +# 05 - length of rest of header +# 00 - multi-path with 1 byte id number +# 03 - length of rest +# idnumber - random id number +# parts - number of parts +# this part - this part, starts from '1' +# +# then AT+CMGS=len-of-TPDU in octets +# +# TPDU header byte: +# Structure: (n) = bits +# +--------+----------+---------+-------+-------+--------+ +# | RP (1) | UDHI (1) | SRI (1) | X (1) | X (1) | MTI(2) | +# +--------+----------+---------+-------+-------+--------+ +# RP: +# Reply path +# UDHI: +# User Data Header Indicator = Does the UD contains a header +# 0 : Only the Short Message +# 1 : Beginning of UD containsheader information +# SRI: +# Status Report Indication. +# The SME (Short Message Entity) has requested a status report. +# MTI: +# 00 for SMS-Deliver +# 01 for SMS-SUBMIT + + +import atchan, sys, re, random + +def encode_number(recipient): + # encoded number is + # number of digits + # 91 for international, 81 for local interpretation + # BCD digits, nibble-swapped, F padded. + if recipient[0] == '+': + type = '91' + recipient = recipient[1:] + else: + type = '81' + leng = '%02X' % len(recipient) + if len(recipient) % 2 == 1: + recipient += 'F' + swap = '' + while recipient: + swap += recipient[1] + recipient[0] + recipient = recipient[2:] + return leng + type + swap + +def code7(pad, mesg): + # Encode the message as 8 chars in 7 bytes. + # pad with 'pad' 0 bits at the start (low in the byte) + carry = 0 + # we have 'pad' low bits stored low in 'carry' + code = '' + while mesg: + c = ord(mesg[0]) + mesg = mesg[1:] + if pad + 7 >= 8: + # have a full byte + b = carry & ((1<> (7-pad) + else: + # not a full byte yet, just a septet + pad = 7 + carry = c + if pad: + # a few bits still to emit + b = carry & ((1<']) + if n == 2: + n,c = chan.chat('%s%s\032' % (dest, mesg), ['OK','ERROR'], 10000) + if n == 0 and atchan.found(c, re.compile('^\+CMGS: \d+') ): + return True + # Sometimes don't get the +CMGS: status - strange + if n == 0: + return True + return False + +recipient = sys.argv[2] +mesg = sys.argv[3] + +chan = atchan.AtChannel(path="/dev/ttyHS_Control") +chan.connect() + +# clear any pending error status +chan.chat1('ATE0', ['OK', 'ERROR']) +if chan.chat1('ATE0', ['OK', 'ERROR']) != 0: + print 'system error - message not sent' + sys.exit(1) + +want = re.compile('^\+COPS:.*".+"') +n,c = chan.chat('AT+COPS?', ['OK', 'ERROR'], 2000 ) +if n != 0 or not atchan.found(c, want): + print 'No Service - message not sent' + sys.exit(1) + +# use PDU mode +n,c = chan.chat('AT+CMGF=0', ['OK', 'ERROR']) +if n != 0: + print 'Unknown error' + sys.exit(1) + +# SMSC header and TPDU header +SMSC = '00' # No SMSC number given, use default +##SMSC = '07911614910930F1' # This is my SMSC +REF = '00' # ask network to assign ref number +phone_num = encode_number(recipient) +proto = '00' # don't know what this means +encode = '00' # 7 bit ascii +if len(mesg) <= 160: + # single SMS mesg + m1 = code7(0,mesg) + m2 = '%02X'%len(mesg) + m1 + coded = "01" + REF + phone_num + proto + encode + m2 + if send(chan, SMSC, coded): + print "OK message sent" + sys.exit(0) + else: + print "ERROR message not sent" + sys.exit(1) + +elif len(mesg) <= 5 * 153: + # Multiple messsage + packets = (len(mesg) + 152) / 153 + packet = 0 + mesgid = random.getrandbits(8) + while len(mesg) > 0: + m = mesg[0:153] + mesg = mesg[153:] + id = mesgid + packet = packet + 1 + UDH = add_len('00' + add_len('%02X%02X%02X'%(id, packets, packet))) + m1 = UDH + code7(1, m) + m2 = '%02X'%(7+len(m)) + m1 + coded = "41" + REF + phone_num + proto + encode + m2 + if not send(chan, SMSC, coded): + print "ERROR message not sent at part %d/%d" % (packet,packets) + sys.exit(1) + print 'OK message sent in %d parts' % packets + sys.exit(0) +else: + print 'Message is too long' + sys.exit(1) + diff --git a/gsm/gsmd-old b/gsm/gsmd-old new file mode 100755 index 0000000..5a09f30 --- /dev/null +++ b/gsm/gsmd-old @@ -0,0 +1,867 @@ +#!/usr/bin/env python + +# +# Calls can be made by writing a number to +# /run/gsm-state/call +# Status get set call 'Calling' and then 'BUSY' or '' +# Call can be answered by writing 'answer' to 'call' +# or can be cancelled by writing ''. +# During a call, chars can be written to +# /run/gsm-state/dtmf +# to send tones. + +## FIXME +# e.g. receive AT response +CREG: 1,"08A7","6E48" +# show that SIM is now ready +# cope with /var/lock/suspend not existing yet +# define 'reset' + +import re, time, gobject, os +from atchan import AtChannel +import dnotify, suspend +from tracing import log +from subprocess import Popen + +def record(key, value): + f = open('/run/gsm-state/.new.' + key, 'w') + f.write(value) + f.close() + os.rename('/run/gsm-state/.new.' + key, + '/run/gsm-state/' + key) + +def recall(key): + try: + fd = open("/run/gsm-state/" + key) + l = fd.read(1000) + fd.close() + except IOError: + l = "" + return l.strip() + +def set_alert(key, value): + path = '/run/alert/' + key + if value == None: + try: + os.unlink(path) + except OSError: + pass + else: + try: + f = open(path, 'w') + f.write(value) + f.close() + except IOError: + pass + +def calllog(key, msg): + f = open('/var/log/' + key, 'a') + now = time.strftime("%Y-%m-%d %H:%M:%S") + f.write(now + ' ' + msg + "\n") + f.close() + +class Task: + def __init__(self, repeat): + self.repeat = repeat + pass + def start(self, channel): + # take the first action for this task + pass + def takeline(self, channel, line): + # a line has arrived that is presumably for us + pass + def timeout(self, channel): + # we asked for a timeout and got it + pass + + +class AtAction(Task): + # An AtAction involves: + # optional sending an AT command to check some value + # matching the result against a string, possibly storing the value + # if there is no match send some other AT command, probably to set a value + # + # States are 'init' 'checking', 'setting', 'done' + ok = re.compile("^OK") + busy = re.compile("\+CMS ERROR.*SIM busy") + not_ok = re.compile("^(ERROR|\+CM[SE] ERROR:)") + def __init__(self, check = None, ok = None, record = None, at = None, + timeout=None, handle = None, repeat = None, arg = None, + critical = True, noreply=None): + Task.__init__(self, repeat) + self.check = check + self.okstr = ok + if ok: + self.okre = re.compile(ok) + self.record = record + self.at = at + self.arg = arg + self.timeout_time = timeout + self.handle = handle + self.critical = critical + self.noreply = noreply + + def start(self, channel): + channel.state['retries'] = 0 + channel.state['stage'] = 'init' + self.advance(channel) + + def takeline(self, channel, line): + if line == None: + channel.set_state('reset') + channel.advance() + return + m = self.ok.match(line) + if m: + channel.cancel_timeout() + if self.handle: + self.handle(channel, line, None) + return self.advance(channel) + + if self.busy.match(line): + channel.cancel_timeout() + channel.set_timeout(5000) + return + if self.not_ok.match(line): + channel.cancel_timeout() + return self.timeout(channel) + + if channel.state['stage'] == 'checking': + m = self.okre.match(line) + if m: + channel.state['matched'] = True + if self.record: + record(self.record[0], m.expand(self.record[1])) + if self.handle: + self.handle(channel, line, m) + return + + if channel.state['stage'] == 'setting': + # didn't really expect anything here.. + pass + + def timeout(self, channel): + if channel.state['retries'] >= 5: + if self.critical: + channel.set_state('reset') + channel.advance() + return + channel.state['retries'] += 1 + channel.state['stage'] = 'init' + channel.atcmd('') + + def advance(self, channel): + st = channel.state['stage'] + if st == 'init' and self.check: + channel.state['stage'] = 'checking' + if self.timeout_time: + channel.atcmd(self.check, timeout = self.timeout_time) + else: + channel.atcmd(self.check) + elif (st == 'init' or st == 'checking') and self.at and not 'matched' in channel.state: + channel.state['stage'] = 'setting' + at = self.at + if self.arg: + at = at % channel.args[self.arg] + if self.timeout_time: + channel.atcmd(at, timeout = self.timeout_time) + else: + channel.atcmd(at) + if self.noreply: + channel.cancel_timeout() + channel.advance() + else: + channel.advance() + +class PowerAction(Task): + # A PowerAction ensure that we have a connection to the modem + # and sets the power on or off, or resets the modem + def __init__(self, cmd): + Task.__init__(self, None) + self.cmd = cmd + + def start(self, channel): + if self.cmd == "on": + if not channel.connected: + channel.connect() + if not channel.altchan.connected: + channel.altchan.connect() + channel.check_flightmode() + elif self.cmd == "off": + record('carrier', '') + record('cell', '') + record('signal_strength','0/32') + channel.disconnect() + channel.altchan.disconnect() + elif self.cmd == 'reopen': + channel.disconnect() + channel.altchan.disconnect() + channel.connect() + channel.altchan.connect() + return channel.advance() + +def rel(handle): + handle.release() + log("did release") + return False + +class SuspendComplete(Task): + # This action simply allows suspend to continue + def __init__(self): + Task.__init__(self, None) + + def start(self, channel): + if channel.suspend_pending: + channel.suspend_pending = False + log("queue release") + gobject.idle_add(rel, channel.suspend_handle) + return channel.advance() + +class ChangeStateAction(Task): + # This action changes to a new state, like a goto + def __init__(self, state): + Task.__init__(self, None) + self.newstate = state + def start(self, channel): + channel.set_state(self.newstate) + return channel.advance() + +class CheckSMS(Task): + def __init__(self): + Task.__init__(self, None) + def start(self, channel): + if channel.pending_sms: + channel.pending_sms = False + p = Popen('gsm-getsms -n', shell=True, close_fds = True) + ok = p.wait() + return channel.advance() + +class RouteVoice(Task): + def __init__(self, on): + Task.__init__(self, None) + self.request = on + def start(self, channel): + if self.request: + channel.sound_on = True + try: + f = open("/run/sound/00-voicecall","w") + f.close() + except: + pass + p = Popen('/usr/local/bin/gsm-voice-routing', close_fds = True) + log('Running gsm-voice-routing pid', p.pid) + channel.voice_route = p + elif channel.sound_on: + if channel.voice_route: + channel.voice_route.send_signal(15) + channel.voice_route.wait() + channel.voice_route = None + try: + os.unlink("/run/sound/00-voicecall") + except OSError: + pass + channel.sound_on = False + return channel.advance() + +class BlockSuspendAction(Task): + def __init__(self, enable): + Task.__init__(self, None) + self.enable = enable + def start(self, channel): + if self.enable: + channel.suspend_blocker.block() + channel.advance() + + if not self.enable: + channel.suspend_blocker.unblock() + + +class Async: + def __init__(self, msg, handle, handle_extra = None): + self.msg = msg + self.msgre = re.compile(msg) + self.handle = handle + self.handle_extra = handle_extra + + def match(self, line): + return self.msgre.match(line) + +# async handlers... +LAC=0 +CELLID=0 +cellnames={} +def status_update(channel, line, m): + if m and m.groups()[3] != None: + global LAC, CELLID, cellnames + LAC = int(m.groups()[2],16) + CELLID = int(m.groups()[3],16) + record('cellid', "%04X %06X" % (LAC, CELLID)); + if CELLID in cellnames: + record('cell', cellnames[CELLID]) + log("That one is", cellnames[CELLID]) + +def new_sms(channel, line, m): + if m: + channel.pending_sms = False + record('newsms', m.groups()[1]) + p = Popen('gsm-getsms -n', shell=True, close_fds = True) + ok = p.wait() +def maybe_sms(line, channel): + channel.pending_sms = True + +def sigstr(channel, line, m): + if m: + record('signal_strength', m.groups()[0] + '/32') + +global incoming_cell_id +def cellid_update(channel, line, m): + # get something like +CBM: 1568,50,1,1,1 + # don't know what that means, just collect the 'extra' line + # I think the '50' means 'this is a cell id'. I should + # probably test for that. + # + # response can be multi-line + global incoming_cell_id + incoming_cell_id = "" + +def cellid_new(channel, line): + global CELLID, cellnames, incoming_cell_id + if not line: + # end of message + if incoming_cell_id: + l = re.sub('[^!-~]+',' ',incoming_cell_id) + if CELLID: + cellnames[CELLID] = l + record('cell', l) + return False + line = line.strip() + if incoming_cell_id: + incoming_cell_id += ' ' + line + else: + incoming_cell_id = line + return True + +incoming_num = None +def incoming(channel, line, m): + global incoming_num + if incoming_num: + record('incoming', incoming_num) + else: + record('incoming', '-') + set_alert('ring', 'new') + if channel.gstate not in ['incoming', 'answer']: + calllog('incoming', '-call-') + channel.set_state('incoming') + record('status', 'INCOMING') + global cpas_zero_cnt + cpas_zero_cnt = 0 + +def incoming_number(channel, line, m): + global incoming_num + if m: + num = m.groups()[0] + if incoming_num == None: + calllog('incoming', num); + incoming_num = num + record('incoming', incoming_num) + +def no_carrier(channel, line, m): + record('status', '') + record('call', '') + if channel.gstate != 'idle': + channel.set_state('idle') + + +def busy(channel, line, m): + record('status', 'BUSY') + record('call', '') + +def ussd(channel, line, m): + pass + +cpas_zero_cnt = 0 +def call_status(channel, line, m): + global cpas_zero_cnt + global calling + log("call_status got", line) + if not m: + return + s = int(m.groups()[0]) + log("s = %d" % s) + if s == 0: + if calling: + return + cpas_zero_cnt += 1 + if cpas_zero_cnt <= 3: + return + # idle + global incoming_num + incoming_num = None + record('incoming', '') + if channel.gstate == 'incoming': + calllog('incoming','-end-') + record('status', '') + if channel.gstate != 'idle' and channel.gstate != 'suspend': + channel.set_state('idle') + cpas_zero_cnt = 0 + calling = False + if s == 3: + # incoming call + if channel.gstate not in ['incoming', 'answer']: + # strange .. + channel.set_state('incoming') + record('status', 'INCOMING') + set_alert('ring', 'new') + record('incoming', '-') + if s == 4: + # on a call + if channel.gstate != 'on-call' and channel.gstate != 'hangup': + channel.set_state('on-call') + +control = {} + +# For flight mode, we turn the power off. +control['to_flight'] = [ + AtAction(at='+CFUN=0'), + PowerAction('off'), + ChangeStateAction('flight'), + ] + +control['flight'] = [ + SuspendComplete() + ] + +control['reset'] = [ + # turning power off just kills everything!!! + PowerAction('reopen'), + #PowerAction('off'), + AtAction(at='E0', timeout=30000), + ChangeStateAction('init'), + ] + +# For suspend, we want power on, but no wakups for status or cellid +control['suspend'] = [ + AtAction(check='+CPAS', ok='\+CPAS: (\d)', handle = call_status), + AtAction(check='+CFUN?', ok='\+CFUN: 1', at='+CFUN=1', timeout=10000), + CheckSMS(), + AtAction(at='+CNMI=1,1,0,0,0'), + AtAction(at='_OSQI=0'), + AtAction(at='_OEANT=0'), + AtAction(at='_OSSYS=0'), + AtAction(at='_OPONI=0'), + AtAction(at='+CREG=0'), + SuspendComplete() + ] + +control['listenerr'] = [ + PowerAction('on'), + AtAction(at='V1E0'), + AtAction(at='+CMEE=2;+CRC=1') + ] +control['init'] = [ + BlockSuspendAction(True), + SuspendComplete(), + PowerAction('on'), + AtAction(at='V1E0'), + AtAction(at='+CMEE=2;+CRC=1'), + # Turn the device on. + AtAction(check='+CFUN?', ok='\+CFUN: 1', at='+CFUN=1', timeout=10000), + # Report carrier as long name + AtAction(at='+COPS=3,0'), + # register with a carrier + #AtAction(check='+COPS?', ok='\+COPS: \d+,\d+,"([^"]*)"', at='+COPS', + # record=('carrier', '\\1'), timeout=10000), + AtAction(check='+COPS?', ok='\+COPS: \d+,\d+,"([^"]*)"', at='+COPS=0', + record=('carrier', '\\1'), timeout=10000, repeat=37000), + # text format for various messages such SMS + AtAction(check='+CMGF?', ok='\+CMGF: 0', at='+CMGF=0'), + # get location status updates + AtAction(at='+CREG=2'), + AtAction(check='+CREG?', ok='\+CREG: 2,(\d)(,"([^"]*)","([^"]*)")', + handle=status_update, timeout=4000), + # Enable collection of Cell Info message + #AtAction(check='+CSCB?', ok='\+CSCB: 1,.*', at='+CSCB=1'), + #AtAction(at='+CSCB=0'), + AtAction(at='+CSCB=1', critical=False), + # Enable async reporting of TXT and Cell info messages + #AtAction(check='+CNMI?', ok='\+CNMI: 1,1,2,0,0', at='+CNMI=1,1,2,0,0'), + AtAction(at='+CNMI=1,0,0,0,0', critical=False), + AtAction(at='+CNMI=1,1,2,0,0', critical=False), + # Enable async reporting of signal strength + AtAction(at='_OSQI=1', critical=False), + + # Enable reporting of Caller number id. + AtAction(check='+CLIP?', ok='\+CLIP: 1,[012]', at='+CLIP=1', timeout=10000, + critical = False), + AtAction(check='+CPAS', ok='\+CPAS: (\d)', handle = call_status), + ChangeStateAction('idle') + ] + +control['idle'] = [ + RouteVoice(False), + CheckSMS(), + BlockSuspendAction(False), + SuspendComplete(), + AtAction(check='+COPS?', ok='\+COPS: \d+,\d+,"([^"]*)"', at='+COPS=0', + record=('carrier', '\\1'), timeout=10000, repeat=37000), + # Make sure on GSM + AtAction(at='_OPSYS=3,2'), + # get signal string + AtAction(check='+CSQ', ok='\+CSQ: (\d+),(\d+)', + record=('signal_strength','\\1/32'), repeat=29000) + ] + +control['incoming'] = [ + BlockSuspendAction(True), + SuspendComplete(), + AtAction(check='+CPAS', ok='\+CPAS: (\d)', handle = call_status, repeat=500), + + # monitor signal strength + AtAction(check='+CSQ', ok='\+CSQ: (\d+),(\d+)', + record=('signal_strength','\\1/32'), repeat=30000) + ] + +control['answer'] = [ + AtAction(at='A'), + RouteVoice(True), + ChangeStateAction('incoming') + ] + +control['call'] = [ + AtAction(at='D%s;', arg='number'), + RouteVoice(True), + ChangeStateAction('on-call') + ] + +control['dtmf'] = [ + AtAction(at='+VTS=%s', arg='dtmf', noreply=True), + ChangeStateAction('on-call') + ] + +control['hangup'] = [ + AtAction(at='+CHUP'), + RouteVoice(False), + BlockSuspendAction(False), + ChangeStateAction('idle') + ] + +control['on-call'] = [ + BlockSuspendAction(True), + SuspendComplete(), + AtAction(check='+CPAS', ok='\+CPAS: (\d)', handle = call_status, repeat=2000), + + # get signal strength + AtAction(check='+CSQ', ok='\+CSQ: (\d+),(\d+)', + record=('signal_strength','\\1/32'), repeat=30000) + ] +async = [ + Async(msg='\+CREG: ([01])(,"([^"]*)","([^"]*)")?', handle=status_update), + Async(msg='\+CMTI: "([A-Z]+)",(\d+)', handle = new_sms), + Async(msg='\+CBM: \d+,\d+,\d+,\d+,\d+', handle=cellid_update, + handle_extra = cellid_new), + Async(msg='\+CRING: (.*)', handle = incoming), + Async(msg='RING', handle = incoming), + Async(msg='\+CLIP: "([^"]+)",[0-9,]*', handle = incoming_number), + Async(msg='NO CARRIER', handle = no_carrier), + Async(msg='BUSY', handle = busy), + Async(msg='\+CUSD: ([012])(,"(.*)"(,[0-9]+)?)?$', handle = ussd), + Async(msg='_OSIGQ: ([0-9]+),([0-9]*)$', handle = sigstr), + + ] + +class GsmD(AtChannel): + + # gsmd works like a state machine + # the high level states are: flight suspend idle incoming on-call + # Note that the whole 'call-waiting' experience is not coverred here. + # That needs to be handled by whoever answers calls and allows interaction + # between user and phone system. + # + # Each state contains a list of tasks such as setting and + # checking config options and monitoring state (e.g. signal strength) + # Some tasks are single-shot and only need to complete each time the state is + # entered. Others are repeating (such as status monitoring). + # We take the first task of the current list and execute it, or wait + # until one will be ready. + # Tasks themselves can be state machines, so we keep track of what 'stage' + # we are up to in the current task. + # + # The system is (naturally) event driven. The main two events that we + # receive are: + # 'takeline' which presents one line of text from the GSM device, and + # 'timeout' which indicates that a timeout set when a command was sent has + # expired. + # Other events are: + # 'taskready' when the time of the next pending task arrives. + # 'flight' when the state of the 'flight mode' has changed + # 'suspend' when a suspend has been requested. + # + # Each event does some event specific processing to modify the state, + # Then calls 'self.advance' to progress the state machine. + # When high level state changes are requested, any pending task is discarded. + # + # If a task detects an error (gsm device not responding properly) it might + # request a reset. This involves sending a modem_reset command and then + # restarting the current state from the top. + # A task can also indicate: + # The next stage to try + # How long to wait before retrying (or None) + # + + def __init__(self, path, altpath): + AtChannel.__init__(self, path = path) + + self.extra = None + self.flightmode = True + self.state = None + self.args = {} + self.suspend_pending = False + self.pending_sms = False + self.sound_on = True + self.voice_route = None + self.tasknum = None + self.altpath = altpath + self.altchan = CarrierDetect(altpath, self) + self.gstate = None + self.nextstate = None + self.statechanged = False + + record('carrier','') + record('cell','') + record('incoming','') + record('signal_strength','') + record('status', '') + + # set the initial state + self.set_state('flight') + + # Monitor other external events which affect us + d = dnotify.dir('/var/lib/misc/flightmode') + self.flightmode_watcher = d.watch('active', self.check_flightmode) + d = dnotify.dir('/run/gsm-state') + self.call_watcher = d.watch('call', self.check_call) + self.dtmf_watcher = d.watch('dtmf', self.check_dtmf) + + self.suspend_handle = suspend.monitor(self.do_suspend, self.do_resume) + self.suspend_blocker = suspend.blocker() + + + # Check the externally imposed state + self.check_flightmode(self.flightmode_watcher) + + # and GO! + self.advance() + + def check_call(self, f = None): + l = recall('call') + log("Check call got", l) + if l == "": + if self.nextstate not in ['hangup', 'idle']: + self.set_state('hangup') + record('status','') + record('incoming','') + elif l == 'answer': + if self.nextstate == 'incoming': + record('status', 'on-call') + record('incoming','') + set_alert('ring', None) + self.set_state('answer') + else: + if self.nextstate == 'idle': + global calling + calling = True + self.args['number'] = l + self.set_state('call') + record('status', 'on-call') + + def check_dtmf(self, f = None): + l = recall('dtmf') + log("Check dtmf got", l) + if len(l): + self.args['dtmf'] = l + self.set_state('dtmf') + record('dtmf','') + + def check_flightmode(self, f = None): + try: + fd = open("/var/lib/misc/flightmode/active") + l = fd.read(1) + fd.close() + except IOError: + l = "" + log("check flightmode got", len(l)) + if len(l) == 0: + if self.flightmode: + self.flightmode = False + if self.suspend_handle.suspended: + self.set_state('suspend') + else: + self.set_state('init') + else: + if not self.flightmode: + self.flightmode = True + self.set_state('to_flight') + + def do_suspend(self): + log("do suspend") + if self.nextstate == 'flight': + return True + self.suspend_pending = True + self.set_state('suspend') + return False + + def do_resume(self): + log("do resume") + if self.nextstate == 'suspend': + self.set_state('init') + + def set_state(self, state): + # this happens asynchronously so we must be careful + # about changing things. Just record the new state + # and abort any timeout + log("state should become", state) + self.nextstate = state + self.statechanged = True + self.abort_timeout() + + def advance(self): + # 'advance' is called by a 'Task' when it has finished + # It may have called 'set_state' first either to report + # an error or to effect a regular state change + now = int(time.time()*1000) + if self.tasknum != None: + self.lastrun[self.tasknum] = now + self.tasknum = None + if self.statechanged: + # time to effect 'set_state' synchronously + self.statechanged = False + self.gstate = self.nextstate + log("state becomes", self.gstate) + n = len(control[self.gstate]) + self.lastrun = n * [0] + (t, delay) = self.next_cmd() + log("advance %s chooses %d, %d" % (self.gstate, t, delay)) + if delay: + log("Sleeping for %f seconds" % (delay/1000.0)) + self.set_timeout(delay) + else: + self.tasknum = t + self.state = {} + control[self.gstate][t].start(self) + + + def takeline(self, line): + + if self.extra: + # an async message is multi-line and we need to handle + # the extra line. + if not self.extra.handle_extra(self, line): + self.extra = None + return False + + if line == None: + self.set_state('reset') + self.advance() + if not line: + return False + + # Check for an async message + for m in async: + mt = m.match(line) + if mt: + m.handle(self, line, mt) + if m.handle_extra: + self.extra = m + return False + + # else pass it to the task + if self.tasknum != None: + control[self.gstate][self.tasknum].takeline(self, line) + + def timedout(self): + if self.tasknum == None: + self.advance() + else: + control[self.gstate][self.tasknum].timeout(self) + + def next_cmd(self): + # Find a command to execute, or a delay + # return (cmd,time) + # cmd is an index into control[state], + # time is seconds until try something + mindelay = 60*60*1000 + cs = control[self.gstate] + n = len(cs) + now = int(time.time()*1000) + for i in range(n): + if self.lastrun[i] == 0 or (cs[i].repeat and + self.lastrun[i] + cs[i].repeat <= now): + return (i, 0) + if cs[i].repeat: + delay = (self.lastrun[i] + cs[i].repeat) - now; + if delay < mindelay: + mindelay = delay + return (0, mindelay) + +class CarrierDetect(AtChannel): + # on the hso modem in the GTA04, the 'NO CARRIER' signal + # arrives on the 'Modem' port, not on the 'Application' port. + # So we listen to the 'Modem' port, and report any + # 'NO CARRIER' we see - or indeed anything that we see. + def __init__(self, path, main): + AtChannel.__init__(self, path = path) + self.main = main + + def takeline(self, line): + self.main.takeline(line) + +class SysfsWatcher: + # watch for changes on a sysfs file and report them + # We read the content, report that, wait for a change + # and report again + def __init__(self, path, action): + self.path = path + self.action = action + self.fd = open(path, "r") + self.watcher = gobject.io_add_watch(self.fd, gobject.IO_PRI, self.read) + self.read() + + def read(self, *args): + self.fd.seek(0) + try: + r = self.fd.read(4096) + except IOerror: + return True + self.action(r) + return True + +try: + os.mkdir("/run/gsm-state") +except: + pass + +calling = False +a = GsmD('/dev/ttyHS_Application', '/dev/ttyHS_Modem') +print "GsmD started" + +try: + f = open("/sys/class/gpio/gpio176/edge", "w") +except IOError: + f = None +if f: + f.write("rising") + f.close() + w = SysfsWatcher("/sys/class/gpio/gpio176/value", + lambda l: maybe_sms(l, a)) +else: + import evdev + def check_evt(dc, mom, typ, code, val): + if typ == 1 and val == 1: + # keypress + maybe_sms("", a) + try: + f = evdev.EvDev("/dev/input/incoming", check_evt) + except: + f = None +c = gobject.main_context_default() +while True: + c.iteration() diff --git a/gsm/gsmd.py b/gsm/gsmd.py new file mode 100644 index 0000000..1d72506 --- /dev/null +++ b/gsm/gsmd.py @@ -0,0 +1,893 @@ +#!/usr/bin/env python + +# +# Calls can be made by writing a number to +# /run/gsm-state/call +# Status get set call 'Calling' and then 'BUSY' or '' +# Call can be answered by writing 'answer' to 'call' +# or can be cancelled by writing ''. +# During a call, chars can be written to +# /run/gsm-state/dtmf +# to send tones. + +## FIXME +# e.g. receive AT response +CREG: 1,"08A7","6E48" +# show that SIM is now ready +# cope with /var/lock/suspend not existing yet +# define 'reset' + +import re, time, gobject, os +from atchan import AtChannel +import dnotify, suspend +from tracing import log +from subprocess import Popen + +def record(key, value): + f = open('/run/gsm-state/.new.' + key, 'w') + f.write(value) + f.close() + os.rename('/run/gsm-state/.new.' + key, + '/run/gsm-state/' + key) + +def recall(key): + try: + fd = open("/run/gsm-state/" + key) + l = fd.read(1000) + fd.close() + except IOError: + l = "" + return l.strip() + +def set_alert(key, value): + path = '/run/alert/' + key + if value == None: + try: + os.unlink(path) + except OSError: + pass + else: + try: + f = open(path, 'w') + f.write(value) + f.close() + except IOError: + pass + +lastlog={} +def calllog(key, msg): + f = open('/var/log/' + key, 'a') + now = time.strftime("%Y-%m-%d %H:%M:%S") + f.write(now + ' ' + msg + "\n") + f.close() + lastlog[key] = msg + +def calllog_end(key): + if key in lastlog: + calllog(key, '-end-') + del lastlog[key] + +class Task: + def __init__(self, repeat): + self.repeat = repeat + pass + def start(self, channel): + # take the first action for this task + pass + def takeline(self, channel, line): + # a line has arrived that is presumably for us + pass + def timeout(self, channel): + # we asked for a timeout and got it + pass + +class AtAction(Task): + # An AtAction involves: + # optionally sending an AT command to check some value + # matching the result against a string, possibly storing the value + # if there is no match send some other AT command, probably to set a value + # + # States are 'init' 'checking', 'setting', 'done' + ok = re.compile("^OK") + busy = re.compile("\+CMS ERROR.*SIM busy") + not_ok = re.compile("^(ERROR|\+CM[SE] ERROR:)") + def __init__(self, check = None, ok = None, record = None, at = None, + timeout=None, handle = None, repeat = None, arg = None, + critical = True, noreply=None): + Task.__init__(self, repeat) + self.check = check + self.okstr = ok + if ok: + self.okre = re.compile(ok) + self.record = record + self.at = at + self.arg = arg + self.timeout_time = timeout + self.handle = handle + self.critical = critical + self.noreply = noreply + + def start(self, channel): + channel.state['retries'] = 0 + channel.state['stage'] = 'init' + self.advance(channel) + + def takeline(self, channel, line): + if line == None: + channel.set_state('reset') + channel.advance() + return + m = self.ok.match(line) + if m: + channel.cancel_timeout() + if self.handle: + self.handle(channel, line, None) + return self.advance(channel) + + if self.busy.match(line): + channel.cancel_timeout() + channel.set_timeout(5000) + return + if self.not_ok.match(line): + channel.cancel_timeout() + return self.timeout(channel) + + if channel.state['stage'] == 'checking': + m = self.okre.match(line) + if m: + channel.state['matched'] = True + if self.record: + record(self.record[0], m.expand(self.record[1])) + if self.handle: + self.handle(channel, line, m) + return + + if channel.state['stage'] == 'setting': + # didn't really expect anything here.. + pass + + def timeout(self, channel): + if channel.state['retries'] >= 5: + if self.critical: + channel.set_state('reset') + channel.advance() + return + channel.state['retries'] += 1 + channel.state['stage'] = 'init' + channel.atcmd('') + + def advance(self, channel): + st = channel.state['stage'] + if st == 'init' and self.check: + channel.state['stage'] = 'checking' + if self.timeout_time: + channel.atcmd(self.check, timeout = self.timeout_time) + else: + channel.atcmd(self.check) + elif (st == 'init' or st == 'checking') and self.at and not 'matched' in channel.state: + channel.state['stage'] = 'setting' + at = self.at + if self.arg: + at = at % channel.args[self.arg] + if self.timeout_time: + channel.atcmd(at, timeout = self.timeout_time) + else: + channel.atcmd(at) + if self.noreply: + channel.cancel_timeout() + channel.advance() + else: + channel.advance() + +class PowerAction(Task): + # A PowerAction ensure that we have a connection to the modem + # and sets the power on or off, or resets the modem + def __init__(self, cmd): + Task.__init__(self, None) + self.cmd = cmd + + def start(self, channel): + if self.cmd == "on": + if not channel.connected: + channel.connect() + if not channel.altchan.connected: + channel.altchan.connect() + channel.check_flightmode() + elif self.cmd == "off": + record('carrier', '') + record('cell', '') + record('signal_strength','0/32') + channel.disconnect() + channel.altchan.disconnect() + elif self.cmd == 'reopen': + channel.disconnect() + channel.altchan.disconnect() + channel.connect() + channel.altchan.connect() + return channel.advance() + +class ChangeStateAction(Task): + # This action changes to a new state, like a goto + def __init__(self, state): + Task.__init__(self, None) + self.newstate = state + def start(self, channel): + if self.newstate: + state = self.newstate + elif channel.statechanged: + state = channel.nextstate + channel.statechanged = False + else: + state = None + if state: + channel.gstate = state + channel.tasknum = None + if not channel.statechanged: + channel.nextstate = state + log("ChangeStateAction chooses", channel.gstate) + n = len(control[channel.gstate]) + channel.lastrun = n * [0] + return channel.advance() + +class CheckSMS(Task): + def __init__(self): + Task.__init__(self, None) + def start(self, channel): + if channel.pending_sms: + channel.pending_sms = False + p = Popen('gsm-getsms -n', shell=True, close_fds = True) + ok = p.wait() + return channel.advance() + +class RouteVoice(Task): + def __init__(self, on): + Task.__init__(self, None) + self.request = on + def start(self, channel): + if self.request: + channel.sound_on = True + try: + f = open("/run/sound/00-voicecall","w") + f.close() + except: + pass + p = Popen('/usr/local/bin/gsm-voice-routing', close_fds = True) + log('Running gsm-voice-routing pid', p.pid) + channel.voice_route = p + elif channel.sound_on: + if channel.voice_route: + channel.voice_route.send_signal(15) + channel.voice_route.wait() + channel.voice_route = None + try: + os.unlink("/run/sound/00-voicecall") + except OSError: + pass + channel.sound_on = False + return channel.advance() + +class BlockSuspendAction(Task): + def __init__(self, enable): + Task.__init__(self, None) + self.enable = enable + def start(self, channel): + print "BlockSuspendAction sets", self.enable + if self.enable: + channel.suspend_blocker.block() + # No point holding a pending suspend any more + if channel.suspend_pending: + channel.suspend_pending = False + print "BlockSuspendAction calls release" + suspend.abort_cycle() + channel.suspend_handle.release() + if not self.enable: + channel.suspend_blocker.unblock() + + channel.advance() + +class Async: + def __init__(self, msg, handle, handle_extra = None): + self.msg = msg + self.msgre = re.compile(msg) + self.handle = handle + self.handle_extra = handle_extra + + def match(self, line): + return self.msgre.match(line) + +# async handlers... +LAC=0 +CELLID=0 +cellnames={} +def status_update(channel, line, m): + if m and m.groups()[3] != None: + global LAC, CELLID, cellnames + LAC = int(m.groups()[2],16) + CELLID = int(m.groups()[3],16) + record('cellid', "%04X %06X" % (LAC, CELLID)); + if CELLID in cellnames: + record('cell', cellnames[CELLID]) + log("That one is", cellnames[CELLID]) + +def new_sms(channel, line, m): + if m: + channel.pending_sms = False + record('newsms', m.groups()[1]) + p = Popen('gsm-getsms -n', shell=True, close_fds = True) + ok = p.wait() + +def maybe_sms(line, channel): + channel.pending_sms = True + +def sigstr(channel, line, m): + if m: + record('signal_strength', m.groups()[0] + '/32') + +global incoming_cell_id +def cellid_update(channel, line, m): + # get something like +CBM: 1568,50,1,1,1 + # don't know what that means, just collect the 'extra' line + # I think the '50' means 'this is a cell id'. I should + # probably test for that. + # + # response can be multi-line + global incoming_cell_id + incoming_cell_id = "" + +def cellid_new(channel, line): + global CELLID, cellnames, incoming_cell_id + if not line: + # end of message + if incoming_cell_id: + l = re.sub('[^!-~]+',' ',incoming_cell_id) + if CELLID: + cellnames[CELLID] = l + record('cell', l) + return False + line = line.strip() + if incoming_cell_id: + incoming_cell_id += ' ' + line + else: + incoming_cell_id = line + return True + +incoming_num = None +def incoming(channel, line, m): + global incoming_num + if incoming_num: + record('incoming', incoming_num) + else: + record('incoming', '-') + set_alert('ring', 'new') + if channel.gstate not in ['on-call', 'incoming', 'answer']: + calllog('incoming', '-call-') + channel.set_state('incoming') + record('status', 'INCOMING') + global cpas_zero_cnt + cpas_zero_cnt = 0 + +def incoming_number(channel, line, m): + global incoming_num + if m: + num = m.groups()[0] + if incoming_num == None: + calllog('incoming', num); + incoming_num = num + record('incoming', incoming_num) + +def no_carrier(channel, line, m): + record('status', '') + record('call', '') + if channel.gstate != 'idle': + channel.set_state('idle') + +def busy(channel, line, m): + record('status', 'BUSY') + record('call', '') + +def ussd(channel, line, m): + pass + +cpas_zero_cnt = 0 +def call_status(channel, line, m): + global cpas_zero_cnt + global calling + log("call_status got", line) + if not m: + return + s = int(m.groups()[0]) + log("s = %d" % s) + if s == 0: + if calling: + return + cpas_zero_cnt += 1 + if cpas_zero_cnt <= 3: + return + # idle + global incoming_num + incoming_num = None + record('incoming', '') + if channel.gstate in ['on-call','incoming','call']: + calllog_end('incoming') + calllog_end('outgoing') + record('status', '') + if channel.gstate != 'idle' and channel.gstate != 'suspend': + channel.set_state('idle') + cpas_zero_cnt = 0 + calling = False + if s == 3: + # incoming call + if channel.gstate not in ['incoming', 'answer']: + # strange .. + channel.set_state('incoming') + record('status', 'INCOMING') + set_alert('ring', 'new') + record('incoming', '-') + if s == 4: + # on a call - but could be just a data call, so don't do anything + #if channel.gstate != 'on-call' and channel.gstate != 'hangup': + # channel.set_state('on-call') + pass + +control = {} + +# For flight mode, we turn the power off. +control['flight'] = [ + AtAction(at='+CFUN=0'), + PowerAction('off'), + BlockSuspendAction(False), + ] + +control['reset'] = [ + # turning power off just kills everything!!! + PowerAction('reopen'), + #PowerAction('off'), + AtAction(at='E0', timeout=30000), + ChangeStateAction('init'), + ] + +# For suspend, we want power on, but no wakups for status or cellid +control['suspend'] = [ + AtAction(check='+CPAS', ok='\+CPAS: (\d)', handle = call_status), + AtAction(check='+CFUN?', ok='\+CFUN: 1', at='+CFUN=1', timeout=10000), + CheckSMS(), + ChangeStateAction(None), # allow async state change + AtAction(at='+CNMI=1,1,0,0,0'), + AtAction(at='_OSQI=0'), + AtAction(at='_OEANT=0'), + AtAction(at='_OSSYS=0'), + AtAction(at='_OPONI=0'), + AtAction(at='+CREG=0'), + ] +control['resume'] = [ + BlockSuspendAction(True), + AtAction(at='+CNMI=1,1,2,0,0', critical=False), + AtAction(at='_OSQI=1', critical=False), + AtAction(at='+CREG=2'), + CheckSMS(), + ChangeStateAction(None), + ChangeStateAction('idle'), + ] + +control['listenerr'] = [ + PowerAction('on'), + AtAction(at='V1E0'), + AtAction(at='+CMEE=2;+CRC=1') + ] +control['init'] = [ + BlockSuspendAction(True), + PowerAction('on'), + AtAction(at='V1E0'), + AtAction(at='+CMEE=2;+CRC=1'), + # Turn the device on. + AtAction(check='+CFUN?', ok='\+CFUN: 1', at='+CFUN=1', timeout=10000), + # Report carrier as long name + AtAction(at='+COPS=3,0'), + # register with a carrier + #AtAction(check='+COPS?', ok='\+COPS: \d+,\d+,"([^"]*)"', at='+COPS', + # record=('carrier', '\\1'), timeout=10000), + AtAction(check='+COPS?', ok='\+COPS: \d+,\d+,"([^"]*)"', at='+COPS=0', + record=('carrier', '\\1'), timeout=10000), + # text format for various messages such SMS + AtAction(check='+CMGF?', ok='\+CMGF: 0', at='+CMGF=0'), + # get location status updates + AtAction(at='+CREG=2'), + AtAction(check='+CREG?', ok='\+CREG: 2,(\d)(,"([^"]*)","([^"]*)")', + handle=status_update, timeout=4000), + # Enable collection of Cell Info message + #AtAction(check='+CSCB?', ok='\+CSCB: 1,.*', at='+CSCB=1'), + #AtAction(at='+CSCB=0'), + AtAction(at='+CSCB=1', critical=False), + # Enable async reporting of TXT and Cell info messages + #AtAction(check='+CNMI?', ok='\+CNMI: 1,1,2,0,0', at='+CNMI=1,1,2,0,0'), + AtAction(at='+CNMI=1,0,0,0,0', critical=False), + AtAction(at='+CNMI=1,1,2,0,0', critical=False), + # Enable async reporting of signal strength + AtAction(at='_OSQI=1', critical=False), + + # Enable reporting of Caller number id. + AtAction(check='+CLIP?', ok='\+CLIP: 1,[012]', at='+CLIP=1', timeout=10000, + critical = False), + AtAction(check='+CPAS', ok='\+CPAS: (\d)', handle = call_status), + ChangeStateAction('idle') + ] + +control['idle'] = [ + RouteVoice(False), + CheckSMS(), + BlockSuspendAction(False), + AtAction(check='+COPS?', ok='\+COPS: \d+,\d+,"([^"]*)"', at='+COPS', + record=('carrier', '\\1'), timeout=10000), + #AtAction(check='+COPS?', ok='\+COPS: \d+,\d+,"([^"]*)"', at='+COPS=0', + # record=('carrier', '\\1'), timeout=10000, repeat=37000), + # Make sure to use both 2G and 3G + AtAction(at='_OPSYS=3,2'), + # get signal string + AtAction(check='+CSQ', ok='\+CSQ: (\d+),(\d+)', + record=('signal_strength','\\1/32'), repeat=29000) + ] + +control['incoming'] = [ + BlockSuspendAction(True), + AtAction(check='+CPAS', ok='\+CPAS: (\d)', handle = call_status, repeat=500), + + # monitor signal strength + AtAction(check='+CSQ', ok='\+CSQ: (\d+),(\d+)', + record=('signal_strength','\\1/32'), repeat=30000) + ] + +control['answer'] = [ + AtAction(at='A'), + RouteVoice(True), + ChangeStateAction('on-call') + ] + +control['call'] = [ + AtAction(at='D%s;', arg='number'), + RouteVoice(True), + ChangeStateAction('on-call') + ] + +control['dtmf'] = [ + AtAction(at='+VTS=%s', arg='dtmf', noreply=True), + ChangeStateAction('on-call') + ] + +control['hangup'] = [ + AtAction(at='+CHUP'), + ChangeStateAction('idle') + ] + +control['on-call'] = [ + BlockSuspendAction(True), + AtAction(check='+CPAS', ok='\+CPAS: (\d)', handle = call_status, repeat=2000), + + # get signal strength + AtAction(check='+CSQ', ok='\+CSQ: (\d+),(\d+)', + record=('signal_strength','\\1/32'), repeat=30000) + ] + +async = [ + Async(msg='\+CREG: ([01])(,"([^"]*)","([^"]*)")?', handle=status_update), + Async(msg='\+CMTI: "([A-Z]+)",(\d+)', handle = new_sms), + Async(msg='\+CBM: \d+,\d+,\d+,\d+,\d+', handle=cellid_update, + handle_extra = cellid_new), + Async(msg='\+CRING: (.*)', handle = incoming), + Async(msg='RING', handle = incoming), + Async(msg='\+CLIP: "([^"]+)",[0-9,]*', handle = incoming_number), + Async(msg='NO CARRIER', handle = no_carrier), + Async(msg='BUSY', handle = busy), + Async(msg='\+CUSD: ([012])(,"(.*)"(,[0-9]+)?)?$', handle = ussd), + Async(msg='_OSIGQ: ([0-9]+),([0-9]*)$', handle = sigstr), + + ] + +class GsmD(AtChannel): + + # gsmd works like a state machine + # the high level states are: flight suspend idle incoming on-call + # Note that the whole 'call-waiting' experience is not coverred here. + # That needs to be handled by whoever answers calls and allows interaction + # between user and phone system. + # + # Each state contains a list of tasks such as setting and + # checking config options and monitoring state (e.g. signal strength) + # Some tasks are single-shot and only need to complete each time the state is + # entered. Others are repeating (such as status monitoring). + # We take the first task of the current list and execute it, or wait + # until one will be ready. + # Tasks themselves can be state machines, so we keep track of what 'stage' + # we are up to in the current task. + # + # The system is (naturally) event driven. The main two events that we + # receive are: + # 'takeline' which presents one line of text from the GSM device, and + # 'timeout' which indicates that a timeout set when a command was sent has + # expired. + # Other events are: + # 'taskready' when the time of the next pending task arrives. + # 'flight' when the state of the 'flight mode' has changed + # 'suspend' when a suspend has been requested. + # + # Each event does some event specific processing to modify the state, + # Then calls 'self.advance' to progress the state machine. + # When high level state changes are requested, any pending task is discarded. + # + # If a task detects an error (gsm device not responding properly) it might + # request a reset. This involves sending a modem_reset command and then + # restarting the current state from the top. + # A task can also indicate: + # The next stage to try + # How long to wait before retrying (or None) + # + + def __init__(self, path, altpath): + AtChannel.__init__(self, path = path) + + self.extra = None + self.flightmode = True + self.state = None + self.args = {} + self.suspend_pending = False + self.pending_sms = False + self.sound_on = True + self.voice_route = None + self.tasknum = None + self.altpath = altpath + self.altchan = CarrierDetect(altpath, self) + self.gstate = None + self.nextstate = None + self.statechanged = False + + record('carrier','') + record('cell','') + record('incoming','') + record('signal_strength','') + record('status', '') + + # set the initial state + self.set_state('flight') + + # Monitor other external events which affect us + d = dnotify.dir('/var/lib/misc/flightmode') + self.flightmode_watcher = d.watch('active', self.check_flightmode) + d = dnotify.dir('/run/gsm-state') + self.call_watcher = d.watch('call', self.check_call) + self.dtmf_watcher = d.watch('dtmf', self.check_dtmf) + + self.suspend_handle = suspend.monitor(self.do_suspend, self.do_resume) + self.suspend_blocker = suspend.blocker() + + # Check the externally imposed state + self.check_flightmode(self.flightmode_watcher) + + # and GO! + self.advance() + + def check_call(self, f = None): + l = recall('call') + log("Check call got", l) + if l == "": + if self.nextstate not in ['hangup', 'idle']: + global incoming_num + incoming_num = None + self.set_state('hangup') + record('status','') + record('incoming','') + calllog_end('incoming') + calllog_end('outgoing') + elif l == 'answer': + if self.nextstate == 'incoming': + record('status', 'on-call') + record('incoming','') + set_alert('ring', None) + self.set_state('answer') + else: + if self.nextstate == 'idle': + global calling + calling = True + self.args['number'] = l + self.set_state('call') + calllog('outgoing',l) + record('status', 'on-call') + + def check_dtmf(self, f = None): + l = recall('dtmf') + log("Check dtmf got", l) + if len(l): + self.args['dtmf'] = l + self.set_state('dtmf') + record('dtmf','') + + def check_flightmode(self, f = None): + try: + fd = open("/var/lib/misc/flightmode/active") + l = fd.read(1) + fd.close() + except IOError: + l = "" + log("check flightmode got", len(l)) + if len(l) == 0: + if self.flightmode: + self.flightmode = False + if self.suspend_handle.suspended: + self.set_state('suspend') + else: + self.set_state('init') + else: + if not self.flightmode: + self.flightmode = True + self.set_state('flight') + + def do_suspend(self): + self.suspend_pending = True + if self.nextstate not in ['flight', 'resume']: + print "do suspend sets suspend" + self.set_state('suspend') + else: + print "do suspend avoids suspend" + self.abort_timeout() + return False + + def do_resume(self): + if self.nextstate == 'suspend': + self.set_state('resume') + + def set_state(self, state): + # this happens asynchronously so we must be careful + # about changing things. Just record the new state + # and abort any timeout + log("state should become", state) + self.nextstate = state + self.statechanged = True + self.abort_timeout() + + def advance(self): + # 'advance' is called by a 'Task' when it has finished + # It may have called 'set_state' first either to report + # an error or to effect a regular state change + now = int(time.time()*1000) + if self.tasknum != None: + self.lastrun[self.tasknum] = now + self.tasknum = None + (t, delay) = self.next_cmd() + log("advance %s chooses %d, %d" % (self.gstate, t, delay)) + if delay and self.statechanged: + # time to effect 'set_state' synchronously + self.statechanged = False + self.gstate = self.nextstate + log("state becomes", self.gstate) + n = len(control[self.gstate]) + self.lastrun = n * [0] + t, delay = self.next_cmd() + + if delay and self.suspend_pending: + self.suspend_pending = False + print "advance calls release" + self.suspend_handle.release() + + if delay: + log("Sleeping for %f seconds" % (delay/1000.0)) + self.set_timeout(delay) + else: + self.tasknum = t + self.state = {} + control[self.gstate][t].start(self) + + def takeline(self, line): + + if self.extra: + # an async message is multi-line and we need to handle + # the extra line. + if not self.extra.handle_extra(self, line): + self.extra = None + return False + + if line == None: + self.set_state('reset') + self.advance() + if not line: + return False + + # Check for an async message + for m in async: + mt = m.match(line) + if mt: + m.handle(self, line, mt) + if m.handle_extra: + self.extra = m + return False + + # else pass it to the task + if self.tasknum != None: + control[self.gstate][self.tasknum].takeline(self, line) + + def timedout(self): + if self.tasknum == None: + self.advance() + else: + control[self.gstate][self.tasknum].timeout(self) + + def next_cmd(self): + # Find a command to execute, or a delay + # return (cmd,time) + # cmd is an index into control[state], + # time is seconds until try something + mindelay = 60*60*1000 + if self.gstate == None: + return (0, mindelay) + cs = control[self.gstate] + n = len(cs) + now = int(time.time()*1000) + for i in range(n): + if self.lastrun[i] == 0 or (cs[i].repeat and + self.lastrun[i] + cs[i].repeat <= now): + return (i, 0) + if cs[i].repeat: + delay = (self.lastrun[i] + cs[i].repeat) - now; + if delay < mindelay: + mindelay = delay + return (0, mindelay) + +class CarrierDetect(AtChannel): + # on the hso modem in the GTA04, the 'NO CARRIER' signal + # arrives on the 'Modem' port, not on the 'Application' port. + # So we listen to the 'Modem' port, and report any + # 'NO CARRIER' we see - or indeed anything that we see. + def __init__(self, path, main): + AtChannel.__init__(self, path = path) + self.main = main + + def takeline(self, line): + self.main.takeline(line) + +class SysfsWatcher: + # watch for changes on a sysfs file and report them + # We read the content, report that, wait for a change + # and report again + def __init__(self, path, action): + self.path = path + self.action = action + self.fd = open(path, "r") + self.watcher = gobject.io_add_watch(self.fd, gobject.IO_PRI, self.read) + self.read() + + def read(self, *args): + self.fd.seek(0) + try: + r = self.fd.read(4096) + except IOerror: + return True + self.action(r) + return True + +try: + os.mkdir("/run/gsm-state") +except: + pass + +calling = False +a = GsmD('/dev/ttyHS_Application', '/dev/ttyHS_Modem') +print "GsmD started" + +try: + f = open("/sys/class/gpio/gpio176/edge", "w") +except IOError: + f = None +if f: + f.write("rising") + f.close() + w = SysfsWatcher("/sys/class/gpio/gpio176/value", + lambda l: maybe_sms(l, a)) +else: + import evdev + def check_evt(dc, mom, typ, code, val): + if typ == 1 and val == 1: + # keypress + maybe_sms("", a) + try: + f = evdev.EvDev("/dev/input/incoming", check_evt) + except: + f = None +c = gobject.main_context_default() +while True: + c.iteration() diff --git a/gsm/notes b/gsm/notes new file mode 100644 index 0000000..dd77cea --- /dev/null +++ b/gsm/notes @@ -0,0 +1,116 @@ +The state machine has become a mess and doesn't work. + +I regularly forget to call 'advance' and I'm not even sure where +it should be called. + +Signals for file changes via dnotify come at arbitrary times and +cause race problems. + +Some states need to progress fully before being changed and there +is no mechanism to ensure that. + +So I need to make it all cleaner. First I need to understand what we have. + + +1/ timeouts + + +2/ response from GSM module + +3/ async message from GSM module + record details + run gsm-getsms + set alerts + +4/ file changes + 'flightmode' + check_flightmode -> suspend or init, or to_flight + 'call' + check_call -> + empty string might hang up + 'answer' should answer if 'incoming' + 'number' should go on-call + + 'dtmf' + switch to 'dtmf' state which then goes back to on-call + +5/ suspend notification: + do_suspend : state to suspend + do_resume : state to init + +states: + + to_flight -> flight + reset + suspend + listenerr + init + idle + incoming + answer + call + dtmf + hangup + on-call + +Actions are: + AtAction - this is only one with stages. + PowerAction + SuspendComplete - should combine with BlockSuspendAction - DONE + ChangeStateAction + CheckSMS + RouteVoice + BlockSuspendAction + + +We have a transition into a state, then being in a state. +so to_flight and flight + call and on-call + answer and on-call + hangup and idle + +Sometimes a sequence of events must complete in order. Sometimes they +can be interrupted. 'suspend' performs a number of actions which +might abort the suspend. e.g. it checks call status. Then it +performs a number of actions that should complete. So maybe have a +'CheckState' action which is allowed to switch state if a request has +been made. This might be implicit at the end of every state, probably +excepting states that have an explicit ChangeStateAction. + +The true states are: + idle + suspend + flight + incoming - blocks suspend + on-call - blocks suspend + +Transitions are: + init : ends in idle + to_flight + reset + answer: incoming -> on-call + hangup: incoming or on-call -> idle + dtmf: on-call -> on-call + call: idle -> on-call + + +Async events check if there is an active task (channel.tasknum). +If there is not, the timer is reset for 'now'. +This is done in an 'idle' handler to ensure single-threaded. +So if tasknum == None, channel.cancel_timeout(), timer_fired() + +if 'advance' finds there is nothing to do but wait, it will allow +suspend to complete if pending. + +----------- + +When we come out of suspend we call init and that somehow does a hangup. +We need to have some wake-from-suspend path that is less intense. + - DONE + +2 probs: +- set_state will interrupt a timeout which can interfere with AtAction + sub states. That is bad. +- resume is immediately replaced by 'suspend' which hangs around longer + than we want... I guess the cpas and cfun and checksms should protect + against staying in suspend... but how? \ No newline at end of file diff --git a/icons/tapinput-dextr.png b/icons/tapinput-dextr.png new file mode 100644 index 0000000000000000000000000000000000000000..3a8ff87b48b9072f739c5d4580abc2c8820fab0b GIT binary patch literal 304 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`Y)RhkE)4%caKYZ?lYt_f1s;*b z3=G^tAk28_ZrvZCAbW|YuPgf-3NJ)lH{J7)(it90Z?!9+SblBr{MdGr`FNHW&m&6B(O1-T{{#(xk_a1U^ zb#YxXnORH8Cni?!`DK5*b*$5$nC0tynfUV29Z1`o vviml3fsNdTZ>N&%ip74M;s5XR{}Jbu=YAcQoDs%Ak1%+;`njxgN@xNAiYj=9 literal 0 HcmV?d00001 diff --git a/lib/tap3 b/lib/tap3 new file mode 100644 index 0000000..0385d8a --- /dev/null +++ b/lib/tap3 @@ -0,0 +1,31 @@ + +Another possible tapboard: +Based on Dextr + +1 ' A B C D , BS +S ? E F G H . DEL +A Z I J K L M RET +SP N O P Q R S SP +SP T U V W X Y SP + + +- + ++ - * { } < > / # [ ] = +@ $ % ^ & * ( ) ~ ` _ \ | +tap esc del home + + ! @ $ % ^ & ` + ( 1 2 3 + ~ ) + [ 4 5 6 - _ ] + { 7 8 9 / \ } + < # 0 * = | > + +Default is 'lower' +drag left/right gives caps +drag left/right then up/down does 'extra' +'1' switches to 'number' mode until pressed again +'A' switches to 'caps' and back + + +Need esc, tab, and other function keyss diff --git a/lib/tapboard.py b/lib/tapboard.py index 0583458..530caa3 100644 --- a/lib/tapboard.py +++ b/lib/tapboard.py @@ -91,7 +91,7 @@ class TapBoard(gtk.VBox): 'hideme' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()) } - def __init__(self): + def __init__(self, mode='lower'): gtk.rc_parse_string(""" style "tap-button-style" { GtkWidget::focus-padding = 0 @@ -163,7 +163,7 @@ class TapBoard(gtk.VBox): # False with '-shift' for a single shift # True with '-shift' for a locked shit self.image_mode = '' - self.mode = 'lower' + self.mode = mode self.shift = '' self.locked = None self.size = 0 diff --git a/lib/tapboard_dextr.py b/lib/tapboard_dextr.py new file mode 100644 index 0000000..30694e6 --- /dev/null +++ b/lib/tapboard_dextr.py @@ -0,0 +1,438 @@ +#!/usr/bin/env python + +# Copyright (C) 2011-2012 Neil Brown +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +# +# a library to draw a widget for tap-input of text +# +# Have a 4x10 array of buttons. Some buttons enter a symbol, +# others switch to a different array of buttons. +# The 4 rows aren't the same, but vary like a qwerty keyboard. +# Row1: 10 buttons +# Row2: 9 buttons offset by half a button +# Row3: 10 buttons just like Row1 +# Row4: 5 buttons each double-width +# +# vertial press/drag is passed to caller as movement. +# press/hold is passed to caller as 'unmap'. +# horizontal press/drag modifies the selected button +# on the first 3 rows, it shifts +# on NUM it goes straight to punctuation +# +# Different configs are: +# lower-case alpha, with minimal punctuation +# upper-case alpha, with different punctuation +# numeric with phone and calculator punctuation +# Remaining punctuation with some cursor control. +# +# Bottom row is normally: +# Shift NUM SPC ENTER BackSpace +# When 'shift' is pressed, the keyboard flips between +# upper/lower or numeric/punc +# and bottom row maybe should become: +# lock control alt ... something. + +import gtk, pango, gobject + +keymap = {} + +keymap['lower'] = [ + ['\'','a','b','c','d',','], + ['?', 'e','f','g','h','.'], + ['z', 'i','j','k','l','m'], + ['n', 'o','p','q','r','s'], + ['t', 'u','v','w','x','y'] +] +keymap['caps'] = [ + ['"', 'A','B','C','D',';'], + ['!', 'E','F','G','H',':'], + ['Z', 'I','J','K','L','M'], + ['N', 'O','P','Q','R','S'], + ['T', 'U','V','W','X','Y'] +] +keymap['lower-shift'] = keymap['caps'] +keymap['lower-xtra'] = [ + [' ',' ',' ',' ',' ',' '], + [' ','1','2','3','+',' '], + [' ','4','5','6','-',' '], + [' ','7','8','9',' ',' '], + [' ','*','0','#',' ',' '] +] +keymap['caps-xtra'] = keymap['lower-xtra'] +keymap['number'] = [ + ['!','@','$','%','^','&','`'], + ['(','1','2','3','+','~',')'], + ['[','4','5','6','-','_',']'], + ['{','7','8','9','/','\\','}'], + ['<','#','0','*','=','|','>'], +] + +class TapBoard(gtk.VBox): + __gsignals__ = { + 'key' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, + (gobject.TYPE_STRING,)), + 'move': (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, + (gobject.TYPE_INT, gobject.TYPE_INT)), + 'hideme' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, + ()) + } + def __init__(self): + gtk.rc_parse_string(""" + style "tap-button-style" { + GtkWidget::focus-padding = 0 + GtkWidget::focus-line-width = 1 + xthickness = 1 + ythickness = 0 + } + widget "*.tap-button" style "tap-button-style" + """) + + gtk.VBox.__init__(self) + self.keysize = 44 + self.aspect = 1.4 + self.width = int(10*self.keysize) + self.height = int(4*self.aspect*self.keysize) + + self.dragx = None + self.dragy = None + self.moved = False + self.xmoved = False + self.xmin = 100000 + self.xmax = 0 + + self.isize = gtk.icon_size_register("mine", 40, 40) + + self.button_timeout = None + + self.buttons = [] + + self.set_homogeneous(True) + + for row in range(5): + h = gtk.HBox() + h.show() + self.add(h) + bl = [] + h.set_homogeneous(True) + for col in range(8): + b = self.add_button(None, self.tap, (row,col), h) + bl.append(b) + self.buttons.append(bl) + + # mode can be 'lower', 'upper', 'func' (later) or 'number' + self.image_mode = '' + self.mode = 'lower' + self.size = 0 + self.connect("size-allocate", self.update_buttons) + self.connect("realize", self.update_buttons) + + + def add_button(self, label, click, arg, box, font = None): + if not label: + b = gtk.Button() + b.set_name("tap-button") + elif label[0:4] == 'gtk-': + img = gtk.image_new_from_stock(label, self.isize) + img.show() + b = gtk.Button() + b.add(img) + else: + b = gtk.Button(label) + b.show() + b.set_property('can-focus', False) + if font: + b.child.modify_font(font) + b.connect('button_press_event', self.press, arg) + b.connect('button_release_event', self.release, click, arg) + b.connect('motion_notify_event', self.motion) + b.add_events(gtk.gdk.POINTER_MOTION_MASK| + gtk.gdk.POINTER_MOTION_HINT_MASK) + + box.add(b) + return b + + def update_buttons(self, *a): + if self.window == None: + return + + alloc = self.buttons[0][0].get_allocation() + w = alloc.width; h = alloc.height + if w > h: + size = h + else: + size = w + size -= 6 + if size <= 10 or size == self.size: + return + #print "update buttons", size + self.size = size + + # For each button in 3x3 we need 10 images, + # one for initial state, and one for each of the new states + # So there are two fonts we want. + # First we make the initial images + fdsingle = pango.FontDescription('sans 10') + fdsingle.set_absolute_size(size / 1.1 * pango.SCALE) + fdword = pango.FontDescription('sans 10') + fdword.set_absolute_size(size / 2 * pango.SCALE) + fdxtra = pango.FontDescription('sans 10') + fdxtra.set_absolute_size(size/2.3 * pango.SCALE) + + bg = self.get_style().bg_gc[gtk.STATE_NORMAL] + fg = self.get_style().fg_gc[gtk.STATE_NORMAL] + red = self.window.new_gc() + red.set_foreground(self.get_colormap().alloc_color(gtk.gdk.color_parse('red'))) + blue = self.window.new_gc() + blue.set_foreground(self.get_colormap().alloc_color(gtk.gdk.color_parse('blue'))) + base_images = {} + for mode in keymap.keys(): + if mode[-5:] == 'xtra': + continue + base_images[mode] = 30*[None] + for row in range(5): + for col in range(6): + if not self.buttons[row][col+1]: + continue + sym = keymap[mode][row][col] + pm = gtk.gdk.Pixmap(self.window, size, size) + pm.draw_rectangle(bg, True, 0, 0, size, size) + if len(sym) == 1: + self.modify_font(fdsingle) + else: + self.modify_font(fdword) + layout = self.create_pango_layout(sym[0:3]) + (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents() + colour = fg + if sym in "aeiouAEUIO0123456789": + colour = blue + pm.draw_layout(colour, + int(size/2 - ew/2), + int(size/2 - eh/2), + layout) + if (mode+'-xtra') in keymap: + self.modify_font(fdxtra) + sym = keymap[mode+'-xtra'][row][col] + layout = self.create_pango_layout(sym) + (ink, (ex,ey,ew2,eh2)) = layout.get_pixel_extents() + colour = fg + if sym in "aeiouAEIOU0123456789": + colour = blue + pm.draw_layout(colour, int(size/2)+ew2,int(size/2)-eh2,layout) + im = gtk.Image() + im.set_from_pixmap(pm, None) + base_images[mode][row*6+col] = im + + base_images['common'] = 24*[None] + for sym in [ (0,0,"123"), (1,0,"ABC"), (2,0,"Func"), + (0,7,"BkSp"), (1,7,"Ret"), (2,7,"Tab")]: + (r,c,s) = sym + pm = gtk.gdk.Pixmap(self.window, size, size) + pm.draw_rectangle(bg, True, 0, 0, size, size) + self.modify_font(fdword) + layout = self.create_pango_layout(s) + (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents() + colour = red + pm.draw_layout(colour, + int(size/2 - ew/2), + int(size/2 - eh/2), + layout) + im = gtk.Image() + im.set_from_pixmap(pm, None) + base_images['common'][r*8+c] = im + + self.base_images = base_images + + fd = pango.FontDescription('sans 10') + fd.set_absolute_size(size / 1.5 * pango.SCALE) + self.modify_font(fd) + self.set_button_images() + + def set_button_images(self): + mode = self.mode + if self.image_mode == mode: + return + for row in range(5): + if row < 3: + for col in [0,7]: + b = self.buttons[row][col] + b.set_image(self.base_images['common'][row*8+col]) + for col in range(6): + b = self.buttons[row][col+1] + if not b: + continue + im = self.base_images[mode][row*6+col] + if im: + b.set_image(im) + self.image_mode = mode + + + def tap(self, rc, moved): + (row,col) = rc + m = self.mode + if col == 0 or col == 7: + # special buttons + if row >= 3: + sym = ' ' + elif rc == (0,7): + sym = '\b' + elif rc == (1,7): + sym = '\n' + elif rc == (2,7): + sym = '\t' + elif rc == (0,0): + if m == 'number': + self.mode = 'lower' + else: + self.mode = 'number' + self.set_button_images() + return + elif rc == (1,0): + if m == 'caps': + self.mode = 'lower' + else: + self.mode = 'caps' + self.set_button_images() + return + else: + return + elif moved: + if moved == 2 and (self.mode + '-xtra') in keymap\ + and keymap[self.mode + '-xtra'][row][col-1] != ' ': + m = self.mode + '-xtra' + else: + m = self.mode + '-shift' + sym = keymap[m][row][col-1] + else: + sym = keymap[m][row][col-1] + self.emit('key', sym) + + def press(self, widget, ev, arg): + self.dragx = int(ev.x_root) + self.dragy = int(ev.y_root) + self.moved = False + self.xmoved = False + self.xmin = self.dragx + self.xmax = self.dragx + + # press-and-hold makes us disappear + if self.button_timeout: + gobject.source_remove(self.button_timeout) + self.button_timeout = None + self.button_timeout = gobject.timeout_add(500, self.disappear) + + def release(self, widget, ev, click, arg): + dx = self.dragx + dy = self.dragy + y = int(ev.y_root) + self.dragx = None + self.dragy = None + + if self.button_timeout: + gobject.source_remove(self.button_timeout) + self.button_timeout = None + if self.moved: + self.moved = False + # If we are half way off the screen, hide + root = gtk.gdk.get_default_root_window() + (rx,ry,rwidth,rheight,depth) = root.get_geometry() + (x,y,width,height,depth) = self.window.get_geometry() + xmid = int(x + width / 2) + ymid = int(y + height / 2) + if xmid < 0 or xmid > rwidth or \ + ymid < 0 or ymid > rheight: + self.hide() + else: + if self.button_timeout: + gobject.source_remove(self.button_timeout) + self.button_timeout = None + if self.xmoved: + if self.xmin < dx and self.xmax > dx: + click(arg, 2) + elif abs(y-dy) > 40: + click(arg, 2) + else: + click(arg, 1) + else: + click(arg, 0) + self.xmoved = False + + def motion(self, widget, ev): + if self.dragx == None: + return + x = int(ev.x_root) + y = int(ev.y_root) + + if (not self.xmoved and abs(y-self.dragy) > 40) or self.moved: + if not self.moved: + self.emit('move', 0, 0) + self.emit('move', x-self.dragx, y-self.dragy) + self.moved = True + if self.button_timeout: + gobject.source_remove(self.button_timeout) + self.button_timeout = None + if (not self.moved and abs(x-self.dragx) > 40) or self.xmoved: + self.xmoved = True + if x < self.xmin: + self.xmin = x + if x > self.xmax: + self.xmax = x + if self.button_timeout: + gobject.source_remove(self.button_timeout) + self.button_timeout = None + if ev.is_hint: + gtk.gdk.flush() + ev.window.get_pointer() + + def disappear(self): + self.dragx = None + self.dragy = None + self.emit('hideme') + + def do_buttons(self): + self.set_button_images() + self.button_timeout = None + return False + + def set_buttons_soon(self): + if self.button_timeout: + gobject.source_remove(self.button_timeout) + self.button_timeout = gobject.timeout_add(500, self.do_buttons) + + +if __name__ == "__main__" : + w = gtk.Window() + w.connect("destroy", lambda w: gtk.main_quit()) + ti = TapBoard() + w.add(ti) + w.set_default_size(ti.width, ti.height) + root = gtk.gdk.get_default_root_window() + (x,y,width,height,depth) = root.get_geometry() + x = int((width-ti.width)/2) + y = int((height-ti.height)/2) + w.move(x,y) + def pkey(ti, str): + print 'key', str + ti.connect('key', pkey) + def hideme(ti): + print 'hidememe' + w.hide() + ti.connect('hideme', hideme) + ti.show() + w.show() + + gtk.main() + diff --git a/lib/wmctrl.py b/lib/wmctrl.py new file mode 100644 index 0000000..f0c8b0e --- /dev/null +++ b/lib/wmctrl.py @@ -0,0 +1,163 @@ + +# +# manage a list of current windows and allow a selected +# window to be raised. +# I'm using Xlib for this, which doesn't have a built-in event +# mechanism like gtk does in gobject. +# So if you want to make sure property change notify events +# get handled, you need to arrange that read events on +# winlist.fd are passed to winlist.events. +# e.g. gobject.io_add_watch(winlist.fd, gobject.IO_IN, winlist.events) +# + +import Xlib.X +import Xlib.display +import Xlib.protocol.event + +class mywindow: + def __init__(self, win, name, pid, id, list): + self.id = id + self.win = win + self.name = name + self.pid = pid + self.list = list + + def raise_win(self): + msg = Xlib.protocol.event.ClientMessage(window = self.win, + client_type = self.list.ACTIVE_WINDOW, + data = (32, [0,0,0,0,0]) + ) + msg.send_event = 1 + mask = (Xlib.X.SubstructureRedirectMask | + Xlib.X.SubstructureNotifyMask) + self.list.root.send_event(msg, event_mask = mask) + self.win.map() + self.win.raise_window() + #p = w.query_tree().parent + #if p: + # p.map() + # p.raise_window() + self.list.display.flush() + + def close_win(self): + msg = Xlib.protocol.event.ClientMessage(window = self.win, + client_type = self.list.CLOSE_WINDOW, + data = (32, [0,0,0,0,0]) + ) + msg.send_event = 1 + mask = (Xlib.X.SubstructureRedirectMask | + Xlib.X.SubstructureNotifyMask) + self.list.root.send_event(msg, event_mask = mask) + self.list.display.flush() + +class winlist: + def __init__(self, add_handle = None): + self.display = Xlib.display.Display() + self.root = self.display.screen().root + self.winfo = {} + self.windows = () + self.WM_STRUT = self.display.intern_atom('_NET_WM_STRUT') + self.CARDINAL = self.display.intern_atom('CARDINAL') + self.ACTIVE_WINDOW = self.display.intern_atom('_NET_ACTIVE_WINDOW') + self.CLOSE_WINDOW = self.display.intern_atom('_NET_CLOSE_WINDOW') + self.NAME = self.display.intern_atom('WM_NAME') + self.STRING = self.display.intern_atom('STRING') + self.PID = self.display.intern_atom('_NET_WM_PID') + self.LIST = self.display.intern_atom('_NET_CLIENT_LIST_STACKING') + self.WINDOW = self.display.intern_atom('WINDOW') + + self.fd = self.display.fileno() + self.change_handle = None + self.add_handle = add_handle + self.del_handle = None + + self.root.change_attributes(event_mask = Xlib.X.PropertyChangeMask ) + self.get_list() + + + def add_win(self, id): + if id in self.winfo: + return self.winfo[id] + w = self.display.create_resource_object('window', id) + p = w.get_property(self.WM_STRUT, self.CARDINAL, 0, 100) + self.winfo[id] = None + if p: + return None + p = w.get_property(self.NAME, self.STRING, 0, 100) + if p and p.format == 8: + name = p.value + name = name.replace('&','&') + name = name.replace('<','<') + name = name.replace('>','>') + else: + return None + + p = w.get_property(self.PID, self.CARDINAL, 0, 100) + if p and p.format == 32: + pid = p.value[0] + else: + pid = 0 + + self.winfo[id] = mywindow(w, name, pid, id, self) + + if self.add_handle: + self.add_handle(self.winfo[id]) + return self.winfo[id] + + + def get_list(self): + l = self.root.get_property(self.LIST, self.WINDOW, 0, 100) + windows = [] + for w in l.value: + if self.add_win(w): + windows.append(w) + self.windows = windows + self.clean_winfo() + if self.change_handle: + self.change_handle() + + def clean_winfo(self): + togo = [] + for w in self.winfo: + if w not in self.windows: + togo.append(w) + for w in togo: + del self.winfo[w] + if self.del_handle: + self.del_handle(w) + + def events(self, *a): + i = self.display.pending_events() + while i > 0: + event = self.display.next_event() + self.handle_event(event) + i = i - 1 + return True + + def handle_event(self, event): + if event.atom != self.LIST: + return False + self.get_list() + return True + + def top(self, num = 0): + if num > len(self.windows) or num < 0: + return None + return self.winfo[self.windows[-1-num]] + + def on_change(self, func, add=None, delete=None): + self.change_handle = func + self.add_handle = add + self.del_handle = delete + + +if __name__ == '__main__': + w = winlist() + for i in w.winfo: + print i, w.winfo[i].name + while 1: + event = w.display.next_event() + if w.handle_event(event): + print "First is", w.top(1).name + w.top(1).raise_win() + diff --git a/netman/dnsmasq.conf b/netman/dnsmasq.conf new file mode 100644 index 0000000..14d4b83 --- /dev/null +++ b/netman/dnsmasq.conf @@ -0,0 +1,3 @@ +dhcp-range=192.168.202.2,192.168.202.6,255.255.255.248,6h +dhcp-range=192.168.202.10,192.168.202.14,255.255.255.248,6h +dhcp-range=192.168.202.18,192.168.202.22,255.255.255.248,6h diff --git a/netman/interfaces b/netman/interfaces new file mode 100644 index 0000000..c74a992 --- /dev/null +++ b/netman/interfaces @@ -0,0 +1,47 @@ + +auto lo +iface lo inet loopback + +auto usb0=usb0-p2p + +# usb0-client is used when no other network is available +iface usb0-client inet static + pre-up rmmod g_ether || true + pre-up modprobe g_ether host_addr=56:88:91:5F:AF:81 + address 192.168.202.1 + netmask 255.255.255.248 + network 192.168.202.0 + gateway 192.168.202.2 + post-down rmmod g_ether + up echo nameserver 192.168.1.3 >/etc/resolv.conf + +# usb0-p2p (peer-to-peer) is used when something else provides +# the default route but we don't want the laptop to use us to get it. +iface usb0-p2p inet static + pre-up rmmod g_ether || true + pre-up modprobe g_ether host_addr=56:88:91:5F:AF:81 + address 192.168.202.1 + netmask 255.255.255.248 + network 192.168.202.0 + post-down rmmod g_ether + +# usb0-hotspot is used when usb is used to tether to a hotspit. +# Note it has a different host_addr so notebook knows to act differently +# and use DHCP to get a route. +iface usb0-hotspot inet static + pre-up rmmod g_ether || true + pre-up modprobe g_ether host_addr=56:88:91:5F:AF:82 + address 192.168.202.1 + netmask 255.255.255.248 + network 192.168.202.0 + post-down rmmod g_ether + +iface wlan0-hotspot inet static + address 192.168.202.9 + netmask 255.255.255.248 + network 192.168.202.8 + +ifbase pan0-hostspot inet static + address 192.168.202.17 + netmask 255.255.255.248 + network 192.168.202.16 diff --git a/netman/netman.py b/netman/netman.py new file mode 100644 index 0000000..6149cc0 --- /dev/null +++ b/netman/netman.py @@ -0,0 +1,737 @@ +#!/usr/bin/env python + +#TODO +# handle iptables masquerade directly so it can be tuned to the IP address +# discovered. +# up iptables -t nat -A POSTROUTING -s 192.168.202.16/29 -j MASQUERADE +# down iptables -t nat -D POSTROUTING -s 192.168.202.16/29 -j MASQUERADE +# This should be done on any interface that is the 'hotspot' +#DONE - rfkill unblock +#DONE - kill children on exit +#- add 3G support +# - connect +# - disconnect +# - poll for status +# - configure access point +#- USB detect presence of connector? +#- wifi: +# - report crypto status and strength +#DONE - extract 'id=' from CONNECTED to find current +#DONE - notice when wifi goes away +# CTRL-EVENT-DISCONNECTED +#DONE - if no dhcp response for a while, reassociate +# - config page to: +# list known and visible networks +# disble/enable, forget, set-password +# make sure to save config +# kill supplicant when done? +#DONE - ensure label is refreshed on different connect and disconnect stages. + +# Manage networks for the openmoko phoenux +# There are 4 devices (unless I add VPN support) +# USB, WIFI, WWAN, Bluetooth +# +# USB is normally on, though it can be turned off to +# allow a different USB widget. +# WIFI, WWAN, Bluetooth need to be explicitly enabled +# for now at least +# There is only one default route, and one DNS server +# If WWAN is active, it provides route and DNS else +# if WIFI is active and DHCP responds, it provides route and DNS +# else if BT is active and DHCP responds, it provides route and DNS +# else USB should provide route and DNS +# +# When we have a route, we provide DHCP to other interfaces +# using dnsmasq, and provide masquarading to Internet. +# +# Main page shows each interface with status including +# IP address +# One can be selected, and bottom buttons can be used to +# enable / disable, hotspot, and configure +# 'configure' goes to a new page, different for each interface. +# +# listen configures the interface to allow incoming... +# +# WWAN: configure allows APN to be set. This is stored per SIM card. +# WIFI: lists known and visible ESSIDs and show password which can be +# editted. Also bottom buttons for 'activate' and 'forget' or 'allow'. +# BT: lists visible peers which support networking and allows 'bind' +# or 'connect' +# USB: ??? +# + +# WWAN in managed using atchan talking to the ttyHS_Control port +# We cannot hold it open, so poll status occasionally. +# ifconfig and route is managed directly. +# WIFI is managed by talking to wpa_supplicant over a socket. +# ifconfig up/down might need to be done directly for power management +# udhcp is managed directly +# BT ?? don't know yet +# USB if managed with ifup/ifdown +# +# dnsmasq is stopped and restarted as needed with required options. +# +# Addresses: +# This program doesn't know about specific IP addresses, that all +# lives in config files. However the plan is as follows: +# Everything lives in a single class-C: 192.168.202.0/24 +# usb-server gets 6 addresses: 0/29 1 is me, 2-7 is you +# usb-client gets same 6 addresses: 0/29 2 is you, 1 is me +# wifi-hotspot gets 6 addresses: 8/29 9 is me, 10-14 are for clients +# bt-server gets 6 addresses: 16/29 17 is me, 18-23 are for clients +# +# dnsmasq only responds to DHCP on 1, 9, 17 +# +# only one of 3g, wifi, bt, usb-client can be active at at time +# They set the server in resolv.conf, and set a default. +# usb-hotspot, wifi-hotspot, bt-hotspot set up IP masqurading. +# +# + +# so... +# Each of 'usb', 'wifi', 'bt', can be in 'hotspot' mode at any time. +# Of 'wifi', 'gsm', 'bt', 'usb', the first that is active and accessable +# and not configured for 'hotspot' is configured as default route and others +# that are not 'hotspot' are disabled - except 'usb' which becomes +# 'p2p' for local traffic only. +# +# An interface can be in one of several states: +# - hotspot (not gsm) - active and providing access +# - disabled - explicitly disabled, not to be used +# - active - has found a connection, provides the default route +# - over-ridden - a higher-precedence interface is active +# - pending - no higher interface is active, but haven't found connection yet. +# +# When 'active' is released, we switch to 'disabled' and deconfig interface +# When 'active' is pressed, we switch to 'pending' and rescan which might +# switch to 'over-ridden', or might start configuring, leading to 'active' +# When 'hotspot' is pressed, we deconfig interface if up, then enable hotspot +# When 'hotspot' is released, we deconfig and switch to 'pending' + + +import gtk, pango +from subprocess import Popen, PIPE +import suspend + +class iface: + def __init__(self, parent): + self.state = 'disabled' + self.parent = parent + self.make_button() + self.can_hotspot = True + self.hotspot_net = None + self.hotspot = False + self.addr = 'No Address' + self.set_label() + + def make_button(self): + self.button = gtk.ToggleButton(self.name) + self.button.set_alignment(0,0.5) + self.button.show() + self.button.connect('pressed', self.press) + + def set_label(self): + self.addr = self.get_addr() + if self.state == 'disabled': + config = '' + else: + self.get_config() + config = ' (%s)' % self.config + if self.hotspot: + hs = 'hotspot ' + else: + hs = '' + self.label = ('%s\n %s%s\n%s\n' + % (self.name, hs, self.state, config)) + if self.state == 'active' and not self.hotspot: + self.label = self.label + 'Gateway: %s' % self.get_default() + self.button.child.set_markup(self.label) + + if (self.hotspot and + self.addr != self.hotspot_net and + self.addr != 'No Address') : + if self.hotspot_net: + Popen(['iptables','-t','nat','-D','POSTROUTING','-s', + self.hotspot_net,'-j','MASQUERADE']).wait() + self.hotspot_net = self.addr + Popen(['iptables','-t','nat','-A','POSTROUTING','-s', + self.hotspot_net,'-j','MASQUERADE']).wait() + if not self.hotspot and self.hotspot_net != None: + Popen(['iptables','-t','nat','-D','POSTROUTING','-s', + self.hotspot_net,'-j','MASQUERADE']).wait() + self.hotspot_net = None + + def get_config(self): + self.config = '-' + + def press(self, ev): + self.parent.select(self) + + def get_addr(self): + p = Popen(['ip', 'add', 'show', 'dev', self.iface], + stdout = PIPE, close_fds = True) + ip = 'No Address' + for l in p.stdout: + l = l.strip().split() + if l[0] == 'inet': + ip = l[1] + p.wait() + return ip + + def get_default(self): + p = Popen(['ip', 'route', 'list', 'match', '128.0.0.1'], + stdout = PIPE, close_fds = True) + route = 'none' + for l in p.stdout: + l = l.strip().split() + if l[0] == 'default' and l[1] == 'via': + route = l[2] + return route + + def rfkill(self, state): + Popen(['rfkill', state, self.rfkill_name]).wait() + + def shutdown(self): + pass + + def config_widget(self): + return None + +from socket import * +import os, gobject, time +from listselect import ListSelect +class WLAN_iface(iface): + # We run wpa_supplicant and udhcpc as needed, killing them when done + # For 'hotspot' we also run 'ifup' or 'ifdown' + # When wpa_supplicant is running, we connect to the socket to + # communicate and add new networks + # Some wpa_cli commands: + # ATTACH -> results in 'OK' + # PING -> results in 'PONG' (wpa_cli does this every 5 seconds) + # LIST_NETWORKS + # 0 LinuxRules any [CURRENT] + # 1 n-squared any + # 2 JesusIsHere any + # 3 TorchNet any + # 4 freedom any + + # + # Some async responses: + # <3>CTRL-EVENT-BSS-ADDED 0 00:60:64:24:0a:22 + # <3>CTRL-EVENT-BSS-ADDED 1 00:04:ed:1e:98:48 + # <3>CTRL-EVENT-SCAN-RESULTS + # <3>Trying to associate with 00:60:64:24:0a:22 (SSID='LinuxRules' freq=2427 MHz) + # <3>Associated with 00:60:64:24:0a:22 + # <3>WPA: Key negotiation completed with 00:60:64:24:0a:22 [PTK=CCMP GTK=TKIP] + # <3>CTRL-EVENT-CONNECTED - Connection to 00:60:64:24:0a:22 completed (auth) [id=0 id_str=] + # <3>CTRL-EVENT-BSS-REMOVED 1 00:04:ed:1e:98:48 + # <3>CTRL-EVENT-DISCONNECTED bssid=..... reason=0 + + def __init__(self, parent): + self.name = 'WLAN' + self.rfkill_name = 'wifi' + self.iface = 'wlan0' + self.ssid = 'unset' + self.supplicant = None + self.udhcpc = None + self.netlist = {} + self.scanlist = {} + self.configing = False + self.pending = None + self.to_send = [] + iface.__init__(self, parent) + self.set_label() + self.cfg = None + + def get_config(self): + self.config = self.addr + ' ESSID=%s' % self.ssid + + def activate(self): + self.rfkill('unblock') + self.supplicant_up() + + def shutdown(self): + self.udhcpc_close() + self.supplicant_close() + Popen('ifconfig wlan0 down', shell=True).wait() + self.rfkill('block') + + def supplicant_up(self): + if self.supplicant: + return + self.supplicant = Popen(['wpa_supplicant','-i','wlan0', + '-c','/etc/wpa_supplicant.conf','-W'], + close_fds = True) + try: + os.unlink('/run/wpa_ctrl_netman') + except: + pass + s = socket(AF_UNIX, SOCK_DGRAM) + s.bind('/run/wpa_ctrl_netman') + ok = False + while not ok: + try: + s.connect('/run/wpa_supplicant/wlan0') + ok = True + except: + time.sleep(0.1) + self.sock = s + self.watch = gobject.io_add_watch(s, gobject.IO_IN, self.recv) + self.request('ATTACH') + + def restart_dhcp(self): + # run udhcpc -i wlan0 -f (in foreground) + # recognise message like + # Lease of 192.168.1.184 obtained, lease time 600 + + self.udhcpc_close() + env = os.environ.copy() + if self.hotspot: + env['NO_DEFAULT'] = 'yes' + self.udhcpc = Popen(['udhcpc','--interface=wlan0','--foreground', + '--script=/etc/wifi-udhcpc.script'], + close_fds = True, + env = env, + stdout = PIPE) + self.udhcpc_watch = gobject.io_add_watch( + self.udhcpc.stdout, gobject.IO_IN, + self.dhcp_read) + + def dhcp_read(self, dir, foo): + if not self.udhcpc: + return False + m = self.udhcpc.stdout.readline() + if not m: + return False + l = m.strip().split() + print 'dhcp got', l + if len(l) >= 4 and l[0] == 'Lease' and l[3] == 'obtained,' and self.state != 'active': + self.state = 'active' + self.parent.update_active() + self.set_label() + if self.checker: + gobject.source_remove(self.checker) + self.checker = None + return True + + def request(self, msg): + if self.pending: + self.to_send.append(msg) + return + self.pending = msg + self.sock.sendall(msg) + print 'sent request', msg + + def recv(self, dir, foo): + try: + r = self.sock.recv(4096) + except error: + return False + if not r: + self.supplicant_close() + return False + + if r[0] == '<': + self.async(r[r.find('>')+1:]) + return True + elif self.pending == 'ATTACH': + # assume it is OK + self.request('LIST_NETWORKS') + elif self.pending == 'LIST_NETWORKS': + self.take_list(r) + elif self.pending == 'SCAN_RESULTS': + self.take_scan(r) + + self.pending = None + if len(self.to_send) > 0: + self.request(self.to_send.pop(0)) + return True + + def udhcpc_close(self): + if self.udhcpc: + gobject.source_remove(self.udhcpc_watch) + self.udhcpc.terminate() + self.udhcpc.wait() + self.udhcpc = None + self.udhcpc_watch = None + + def supplicant_close(self): + if self.supplicant: + self.supplicant.terminate() + gobject.source_remove(self.watch) + self.watch = None + self.supplicant.wait() + self.supplicant = None + + def async(self, mesg): + print 'GOT ', mesg + print 'got (%s)' % mesg[:20] + if mesg[:20] == 'CTRL-EVENT-CONNECTED': + p = mesg.find('id=') + if p > 0: + id = mesg[p+3:].split()[0] + for ssid in self.netlist: + if id == self.netlist[ssid]: + self.ssid = ssid + self.set_label() + self.checker = gobject.timeout_add(30000, self.reassoc) + return self.restart_dhcp() + if mesg[:23] == 'CTRL-EVENT-DISCONNECTED': + if self.state == 'active': + self.state = 'pending' + self.udhcpc_close() + self.set_label() + self.parent.update_active() + return + if mesg[:23] == 'CTRL-EVENT-SCAN-RESULTS': + self.request('SCAN_RESULTS') + return + if mesg[:19] == 'Trying to associate': + l = mesg.split("'") + print 'len', len(l) + if len(l) > 1: + print 'ssid = ', l[1] + self.ssid = l[1] + return + + def reassoc(self): + if self.state == 'active': + return False + self.request('REASSOCIATE') + self.checker = gobject.timeout_add(30000, self.reassoc) + return False + + def take_list(self, mesg): + self.netlist = {} + for line in mesg.split('\n'): + if line != '' and line[0:7] != 'network': + l = line.split('\t') + self.netlist[l[1]] = l[0] + print 'netlist', self.netlist + self.fill_list() + + def take_scan(self, mesg): + self.scanlist = {} + for line in mesg.split('\n'): + if line != '' and line[:5] != 'bssid': + l = line.split('\t') + self.scanlist[l[4]] = (l[2],l[3]) + print 'scan', self.scanlist + self.fill_list() + + def config_widget(self): + if self.cfg == None: + self.make_cfg() + self.fill_list() + self.configing = True + self.supplicant_up() + self.request('SCAN') + return self.cfg + + def make_cfg(self): + # config widget is an entry, a ListSel, and a button row + cfg = gtk.VBox() + entry = gtk.Entry() + ls = ListSelect() + bb = gtk.HBox() + + cfg.show() + cfg.pack_start(entry, expand = False) + entry.show() + + cfg.pack_start(ls, expand = True) + ls.show() + ls.set_zoom(40) + cfg.pack_start(bb, expand = False) + bb.show() + bb.set_homogeneous(True) + bb.set_size_request(-1, 80) + + entry.modify_font(self.parent.fd) + self.add_button(bb, 'Allow', self.allow) + self.add_button(bb, 'Forget', self.forget) + self.add_button(bb, 'Set Key', self.set_key) + self.add_button(bb, 'Done', self.done) + + self.cfg = cfg + self.key_entry = entry + self.net_ls = ls + + def add_button(self, bb, name, cmd): + btn = gtk.Button() + btn.set_label(name) + btn.child.modify_font(self.parent.fd) + btn.show() + bb.pack_start(btn, expand = True) + btn.connect('clicked', cmd) + + def allow(self, x): + self.net_ls.reconfig(self.net_ls,1) + print 'alloc is', self.net_ls.get_allocation() + pass + def forget(self, x): + pass + def set_key(self, x): + pass + def done(self, x): + self.configing = False + self.parent.deconfig() + + def fill_list(self): + if not self.configing: + return + l = [('none','green')] + for i in self.scanlist: + if i in self.netlist: + l.append((i, 'blue')) + else: + l.append((i, 'black')) + for i in self.netlist: + if i not in self.scanlist: + l.append((i, 'red')) + print 'filled list', l + self.net_ls.list = l + self.net_ls.list_changed() + + def config_hotspot(self): + self.rfkill('unblock') + self.supplicant_up() + + +class WWAN_iface(iface): + def __init__(self, parent): + self.name = 'GSM/3G' + self.rfkill_name = 'wwan' + self.conf = None + self.iface = 'hso0' + self.APN = 'INTERNET' + iface.__init__(self, parent) + self.can_hotspot = False + + def get_config(self): + self.config = self.addr + ' APN=%s' % self.APN + + def activate(self): + self.rfkill('unblock') + Popen('/usr/local/bin/gsm-data up', shell=True).wait() + self.state = 'active' + + def shutdown(self): + Popen('/usr/local/bin/gsm-data down', shell=True).wait() + self.rfkill('block') + +class BT_iface(iface): + def __init__(self, parent): + self.name = 'BT' + self.iface = 'pan0' + self.rfkill_name = 'bluetooth' + iface.__init__(self, parent) + +class USB_iface(iface): + def __init__(self, parent): + self.name = 'USB' + self.conf = 'p2p' + self.iface = 'usb0' + self.state = 'pending' + iface.__init__(self, parent) + self.set_label() + + def get_config(self): + self.config = self.addr + + def setconf(self, mode): + if self.conf == mode: + return + if mode: + cmd = 'ifdown --force usb0; ifup usb0=usb0-%s' % mode + else: + cmd = 'ifdown --force usb0' + print 'run command', cmd + Popen(cmd, shell=True).wait() + self.conf = mode + + def shutdown(self): + # interpret this as 'switch to p2p mode', unless 'disabled' + if self.state == 'disabled': + self.setconf(None) + else: + self.setconf('p2p') + + def activate(self): + # must set state to 'active' once it is - then maybe update_active + print 'activate usb' + self.setconf('client') + self.state = 'active' + + def config_hotspot(self): + self.setconf('hotspot') + self.state = 'active' + +class Netman(gtk.Window): + # Main window has one radio button for each interface. + # Along bottom are 3 buttons: toggle 'enable', toggle 'hotspot', 'config' + # + def __init__(self): + gtk.Window.__init__(self) + self.connect('destroy', self.close_application) + self.set_title('NetMan') + + self.iface = [ WLAN_iface(self), WWAN_iface(self), + BT_iface(self), USB_iface(self) ] + + # 'v' fills the window, 'i' holds the interface, 'b' holds the buttons + v = gtk.VBox() + v.show() + self.main_window = v + self.add(v) + i = gtk.VBox() + i.show() + v.pack_start(i, expand = True) + i.set_homogeneous(True) + + for iface in self.iface: + i.pack_start(iface.button, expand = True) + + b = gtk.HBox() + b.show() + b.set_size_request(-1, 80) + b.set_homogeneous(True) + v.pack_end(b, expand = False) + + ctx = self.get_pango_context() + fd = ctx.get_font_description() + fd.set_absolute_size(35*pango.SCALE) + self.fd = fd + + self.blocker = suspend.blocker() + + self.enable_btn = self.add_button(b, 'enabled', fd, True, self.enable) + self.hotspot_btn = self.add_button(b, 'hotspot', fd, True, self.hotspot) + self.add_button(b, 'config', fd, False, self.config) + self.selected = self.iface[0] + self.iface[0].button.set_active(True) + + self.update_active() + + def close_application(self, ev): + for i in self.iface: + i.shutdown() + gtk.main_quit() + + def select(self, child): + for i in self.iface: + if i != child: + i.button.set_active(False) + self.selected = None + self.enable_btn.set_active(child.state != 'disabled') + self.hotspot_btn.set_inconsistent(not child.can_hotspot) + self.hotspot_btn.set_active(child.hotspot) + self.selected = child + + def add_button(self, bb, name, fd, toggle, func): + if toggle: + btn = gtk.ToggleButton() + else: + btn = gtk.Button() + btn.set_label(name) + btn.child.modify_font(fd) + btn.show() + bb.pack_start(btn, expand = True) + btn.connect('clicked', func) + return btn + + def enable(self, ev): + if not self.selected: + return + print 'active?', self.enable_btn.get_active() + if not self.enable_btn.get_active(): + return self.disable() + + if self.selected.state != 'disabled': + return + print 'enable %s, was %s' % (self.selected.name, self.selected.state) + self.selected.state = 'pending' + self.update_active() + def disable(self): + if self.selected.state == 'disabled': + return + self.selected.state = 'disabled' + self.selected.shutdown() + self.update_active() + self.selected.set_label() + + def hotspot(self, ev): + if not self.selected: + return + if self.hotspot_btn.get_active(): + # enable hotspot + s = self.selected + self.selected = None + self.enable_btn.set_active(True) + self.selected = s + s.hotspot = True + try: + f = open('/proc/sys/net/ipv4/ip_forward','w') + f.write('1') + f.close() + except: + pass + s.config_hotspot() + s.set_label() + self.blocker.block() + else: + # disable hotspot + self.selected.hotspot = False + self.selected.shutdown() + self.selected.set_label() + self.update_active() + + def config(self, ev): + w = self.selected.config_widget() + if not w: + return + self.remove(self.main_window) + self.add(w) + w.show() + + def deconfig(self): + self.remove(self.get_child()) + self.add(self.main_window) + + def update_active(self): + # try to activate the first available interface, + # and stop others + active_found = False + print 'update_active start' + hotspot = False + for i in self.iface: + print i.name, i.state + if i.hotspot: + hotspot = True + if i.state in ['disabled'] or i.hotspot: + continue + if active_found: + if i.state != 'over-ridden': + i.shutdown() + i.state = 'over-ridden' + i.set_label() + elif i.state == 'active': + active_found = True + else: + if i.state == 'over-ridden': + i.state = 'pending' + i.activate() + if i.state == 'active': + active_found = True + i.set_label() + if not hotspot: + self.blocker.unblock() + print 'update_active end' + +if __name__ == '__main__': + + n = Netman() + n.set_default_size(480, 640) + n.show() + gtk.main() + diff --git a/netman/sysctl.conf b/netman/sysctl.conf new file mode 100644 index 0000000..c24ade9 --- /dev/null +++ b/netman/sysctl.conf @@ -0,0 +1,2 @@ +net.ipv4.ip_forward=1 +net.ipv6.conf.all.forwarding=1 diff --git a/netman/usbnet b/netman/usbnet new file mode 100644 index 0000000..31ccd45 --- /dev/null +++ b/netman/usbnet @@ -0,0 +1,8 @@ +#!/bin/sh +ifconfig wlan0 down +ifdown usb0 +if [ " $1" = " on" ] +then + ifup usb0 + ifconfig usb0 +fi diff --git a/netman/wifi-udhcpc.script b/netman/wifi-udhcpc.script new file mode 100755 index 0000000..2c1a186 --- /dev/null +++ b/netman/wifi-udhcpc.script @@ -0,0 +1,72 @@ +#!/bin/sh +# udhcpc script for netman - modified to recognised NO_DEFAULT env var +# to mean that default route and DNS config should not be included. +# +# Busybox udhcpc dispatcher script. Copyright (C) 2009 by Axel Beckert. +# +# Based on the busybox example scripts and the old udhcp source +# package default.* scripts. + +RESOLV_CONF="/etc/resolv.conf" + +echo udhcp script $* + +case $1 in + bound|renew) + [ -n "$broadcast" ] && BROADCAST="broadcast $broadcast" + [ -n "$subnet" ] && NETMASK="netmask $subnet" + + /sbin/ifconfig $interface $ip $BROADCAST $NETMASK + + if [ -z "$NO_DEFAULT" ]; then + if [ -n "$router" ]; then + echo "$0: Resetting default routes" + while /sbin/route del default gw 0.0.0.0 dev $interface; do :; done + + metric=0 + for i in $router; do + /sbin/route add default gw $i dev $interface metric $metric + metric=$(($metric + 1)) + done + fi + + # Update resolver configuration file + R="" + [ -n "$domain" ] && R="domain $domain +" + for i in $dns; do + echo "$0: Adding DNS $i" + R="${R}nameserver $i +" + done + + if [ -x /sbin/resolvconf ]; then + echo -n "$R" | resolvconf -a "${interface}.udhcpc" + else + echo -n "$R" > "$RESOLV_CONF" + fi + fi + ;; + + deconfig) + if [ -z "$NO_DEFAULT" ]; then + if [ -x /sbin/resolvconf ]; then + resolvconf -d "${interface}.udhcpc" + fi + fi + /sbin/ifconfig $interface 0.0.0.0 + ;; + + leasefail) + echo "$0: Lease failed: $message" + ;; + + nak) + echo "$0: Received a NAK: $message" + ;; + + *) + echo "$0: Unknown udhcpc command: $1"; + exit 1; + ;; +esac diff --git a/netman/wifinet b/netman/wifinet new file mode 100644 index 0000000..34b77d6 --- /dev/null +++ b/netman/wifinet @@ -0,0 +1,16 @@ +#!/bin/sh + +ifdown usb0 +ifconfig wlan0 down +fuser -k /sbin/wpa_* +rmmod libertas_sdio +if [ " $1" = " up" ] +then + modprobe libertas_sdio + rfkill unblock wifi + wpa_supplicant -B -i wlan0 -c /etc/wpa_supplicant.conf -W + wpa_cli -B -i wlan0 -a /root/wpa_updown + sleep 5 + ifconfig wlan0 + iwconfig wlan0 +fi diff --git a/petrol/petrol.py b/petrol/petrol.py new file mode 100644 index 0000000..d7d6d4d --- /dev/null +++ b/petrol/petrol.py @@ -0,0 +1,437 @@ +#!/usr/bin/env python + +# +# Freerunner app to track petrol usage in new car. +# We need to keep a log of entries. Each entry: +# +# date kilometers litres whether-full price-paid +# +# These are displayed with l/100K number and can get +# overall l/100K and c/l between two points +# +# Must be able to edit old entries. +# +# So: 2 pages: +# +# 1/ +# summary line: l/100K, c/l from selected to mark +# list of entries, scrollable and selectable +# buttons: new, edit, mark/unmark +# +# 2/ Entry fields for a new entry +# date: default to 'today' with plus/minus button on either side +# kilometer - simple text entry +# litres + 'fill' indicator +# c/l +# $ +# keyboard in number mode +# Buttons: Fill, not-fill, Discard, Save +# +# Should I be able to select between different vehicles? Not now. + +import sys, os, time +import pygtk, gtk, pango +from listselect import ListSelect +from tapboard import TapBoard + + +class petrol_list: + def __init__(self, list): + self.list = list + self.mark = None + def set_list(self, list): + self.list = list + def set_mark(self, ind): + self.mark = ind + + def __len__(self): + return len(self.list) + def __getitem__(self, ind): + i = self.list[ind] + dt = i[0] + tm = time.strptime(dt, '%Y-%m-%d') + dt = time.strftime('%d/%b/%Y', tm) + k = "%06d" % int(i[1]) + l = "%06.2f" % float(i[2]) + if len(i) > 3 and i[3] == "full": + f = "F" + else: + f = "-" + if len(i) > 4: + p = "$%06.2f" % float(i[4]) + else: + p = "----.--" + str = "%s %s %s %s %s" % (dt, k, l, p, f) + if self.mark == ind: + type = 'mark' + else: + type = 'normal' + return (str, type) + + + +class Petrol(gtk.Window): + def __init__(self, file): + gtk.Window.__init__(self) + self.connect("destroy",self.close_application) + self.set_title("Petrol") + + ctx = self.get_pango_context() + fd = ctx.get_font_description() + fd.set_absolute_size(35*pango.SCALE) + vb = gtk.VBox(); self.add(vb); vb.show() + self.isize = gtk.icon_size_register("petrol", 40, 40) + + self.listui = self.make_list_ui(fd) + self.editui = self.make_edit_ui(fd) + vb.add(self.listui); vb.add(self.editui) + self.listui.show() + self.editui.hide() + + self.active_entry = None + self.colours={} + + self.filename = file + self.load_file() + self.pl.set_list(self.list) + + def close_application(self, ev): + gtk.main_quit() + + def make_list_ui(self, fd): + ui = gtk.VBox() + l = gtk.Label("Petrol Usage") + l.modify_font(fd) + l.show(); ui.pack_start(l, expand=False) + + h = gtk.HBox(); h.show(); ui.pack_start(h, expand=False) + h.set_homogeneous(True) + ui.usage_summary = gtk.Label("??.??? l/CKm") + ui.usage_summary.show() + ui.usage_summary.modify_font(fd) + h.add(ui.usage_summary) + + ui.price_summary = gtk.Label("???.? c/l") + ui.price_summary.show() + ui.price_summary.modify_font(fd) + h.add(ui.price_summary) + + ui.list = ListSelect() + ui.list.show() + ui.pack_start(ui.list, expand=True) + ui.list.set_format("normal","black", background="grey", selected="white") + ui.list.set_format("mark", "black", bullet=True, + background="grey", selected="white") + ui.list.set_format("blank", "black", background="lightblue") + ui.list.connect('selected', self.selected) + self.pl = petrol_list([]) + ui.list.list = self.pl + ui.list.set_zoom(34) + + bbar = gtk.HBox(); bbar.show(); ui.pack_start(bbar, expand=False) + self.button(bbar, "New", fd, self.new) + self.button(bbar, "Edit", fd, self.edit) + self.button(bbar, "Mark", fd, self.mark) + return ui + + def make_edit_ui(self, fd): + ui = gtk.VBox() + + # title + l = gtk.Label("Petrol Event") + l.modify_font(fd) + l.show(); ui.pack_start(l, fill=False) + + # date - with prev/next buttons. + h = gtk.HBox(); h.show(); ui.pack_start(h,fill=False) + self.button(h, gtk.STOCK_GO_BACK, fd, self.date_prev, expand=False) + l = gtk.Label("Today") + l.modify_font(fd) + l.show(); h.pack_start(l, expand=True) + self.button(h, gtk.STOCK_GO_FORWARD, fd, self.date_next, expand=False) + ui.date = l + + # text entry for kilometers + h = gtk.HBox(); h.show(); ui.pack_start(h,fill=False) + e = self.entry(h, "Km:", fd, self.KM) + self.km_entry = e; + + # text entry for litres, with 'fill' indicator + h = gtk.HBox(); h.show(); ui.pack_start(h,fill=False) + e = self.entry(h, "litres:", fd, self.Litres) + self.l_entry = e; + l = gtk.Label("(full)") + l.modify_font(fd) + l.show(); h.pack_start(l, expand=False) + self.full_label = l + + # text entry for cents/litre + h = gtk.HBox(); h.show(); ui.pack_start(h,fill=False) + e = self.entry(h, "cents/l:", fd, self.Cents) + self.cl_entry = e; + + # text entry for price paid + h = gtk.HBox(); h.show(); ui.pack_start(h,fill=False) + e = self.entry(h, "Cost: $", fd, self.Cost) + self.cost_entry = e; + + self.entry_priority = [self.l_entry, self.cl_entry] + + # keyboard + t = TapBoard(); t.show() + ui.pack_start(t, fill=True) + t.connect('key', self.use_key) + + # Buttons: fill/non-fill, Discard, Save + bbar = gtk.HBox(); bbar.show(); ui.pack_start(bbar, fill=False) + bbar.set_homogeneous(True) + self.fill_button = self.button(bbar, "non-Fill", fd, self.fill) + self.button(bbar, "Discard", fd, self.discard) + self.button(bbar, "Save", fd, self.save) + + return ui + + def button(self, bar, label, fd, func, expand = True): + btn = gtk.Button() + if label[0:3] == "gtk" : + img = gtk.image_new_from_stock(label, self.isize) + img.show() + btn.add(img) + else: + btn.set_label(label) + btn.child.modify_font(fd) + btn.show() + bar.pack_start(btn, expand = expand) + btn.connect("clicked", func) + btn.set_focus_on_click(False) + return btn + + def entry(self, bar, label, fd, func): + l = gtk.Label(label) + l.modify_font(fd) + l.show(); bar.pack_start(l, expand=False) + e = gtk.Entry(); e.show(); bar.pack_start(e, expand=True) + e.modify_font(fd) + e.set_events(e.get_events()|gtk.gdk.FOCUS_CHANGE_MASK) + e.connect('focus-in-event', self.focus) + e.connect('changed', func) + return e + def focus(self, ent, ev): + self.active_entry = ent + if (len(self.entry_priority) == 0 or + self.entry_priority[0] != ent): + # Make this entry the most recent + self.entry_priority = [ent] + self.entry_priority[:1] + + def calc_other(self): + if len(self.entry_priority) != 2: + return + if self.l_entry not in self.entry_priority: + cl = self.check_entry(self.cl_entry) + cost = self.check_entry(self.cost_entry) + if cl != None and cost != None: + self.force_entry(self.l_entry, "%.2f" %(cost * 100.0 / cl)) + if self.cl_entry not in self.entry_priority: + l = self.check_entry(self.l_entry) + cost = self.check_entry(self.cost_entry) + if l != None and l > 0 and cost != None: + self.force_entry(self.cl_entry, "%.2f" %(cost * 100.0 / l)) + if self.cost_entry not in self.entry_priority: + cl = self.check_entry(self.cl_entry) + l = self.check_entry(self.l_entry) + if cl != None and l != None: + self.force_entry(self.cost_entry, "%.2f" %(cl * l / 100.0)) + + def force_entry(self, entry, val): + entry.set_text(val) + entry.modify_base(gtk.STATE_NORMAL, self.get_colour('yellow')) + + def use_key(self, tb, str): + if not self.active_entry: + return + if str == '\b': + self.active_entry.emit('backspace') + elif str == '\n': + self.active_entry.emit('activate') + else: + self.active_entry.emit('insert-at-cursor', str) + + def set_colour(self, name, col): + self.colours[name] = col + def get_colour(self, col): + # col is either a colour name, or a pre-set colour. + # so if it isn't in the list, add it + if col == None: + return None + if col not in self.colours: + self.set_colour(col, col) + if type(self.colours[col]) == str: + self.colours[col] = \ + self.get_colormap().alloc_color(gtk.gdk.color_parse(self.colours[col])) + return self.colours[col] + + + def check_entry(self, entry): + txt = entry.get_text() + v = None + try: + if txt != "": + v = eval(txt) + colour = None + except: + colour = 'red' + entry.modify_base(gtk.STATE_NORMAL, self.get_colour(colour)) + return v + + def KM(self,entry): + v = self.check_entry(entry) + def Litres(self,entry): + self.calc_other() + def Cents(self,entry): + self.calc_other() + def Cost(self,entry): + self.calc_other() + + def load_file(self): + # date:km:litre:full?:price + list = [] + try: + f = open(self.filename) + l = f.readline() + while len(l) > 0: + l = l.strip() + w = l.split(':') + if len(w) >= 3: + list.append(w) + l = f.readline() + except: + pass + + list.sort(reverse=True) + self.list = list + self.curr = 0 + self.mark = None + + def save_file(self): + try: + f = open(self.filename + '.tmp', 'w') + for l in self.list: + f.write(':'.join(l) + '\n') + f.close() + os.rename(self.filename + '.tmp', self.filename) + except: + pass + + def selected(self, ev, ind): + self.curr = ind + l = self.list[ind] + ind+= 1 + ltr = float(l[2]) + while ind < len(self.list) and \ + (len(self.list[ind]) <= 3 or self.list[ind][3] != 'full'): + ltr += float(self.list[ind][2]) + ind += 1 + if ind >= len(self.list) or len(l) <= 3 or l[3] != 'full': + lckm = "??.??? l/CKm" + else: + km = float(l[1]) - float(self.list[ind][1]) + lckm = "%6.3f l/CKm" % (ltr*100/km) + self.listui.usage_summary.set_text(lckm) + + if len(l) >= 5 and float(l[2]) > 0: + cl = "%6.2f c/l" % (float(l[4])*100/float(l[2])) + else: + cl = "???.? c/l" + self.listui.price_summary.set_text(cl) + + def new(self, ev): + self.curr = None + self.editui.date.set_text('Today') + self.km_entry.set_text('') + self.l_entry.set_text('') + self.full_label.set_text('(full)') + self.cl_entry.set_text('') + self.cost_entry.set_text('') + self.listui.hide() + self.editui.show() + + def edit(self, ev): + if self.curr == None: + self.curr = 0 + l = self.list[self.curr] + self.editui.date.set_text(time.strftime('%d/%b/%Y', time.strptime(l[0], "%Y-%m-%d"))) + self.km_entry.set_text(l[1]) + self.l_entry.set_text(l[2]) + self.full_label.set_text('(full)' if l[3]=='full' else '(not full)') + self.cost_entry.set_text(l[4]) + self.entry_priority=[self.l_entry, self.cost_entry]; + self.calc_other() + self.listui.hide() + self.editui.show() + + def mark(self, ev): + pass + + def date_get(self): + x = self.editui.date.get_text() + try: + then = time.strptime(x, "%d/%b/%Y") + except ValueError: + then = time.localtime() + return then + def date_prev(self, ev): + tm = time.localtime(time.mktime(self.date_get()) - 12*3600) + self.editui.date.set_text(time.strftime("%d/%b/%Y", tm)) + + def date_next(self, ev): + t = time.mktime(self.date_get()) + 25*3600 + if t > time.time(): + t = time.time() + tm = time.localtime(t) + self.editui.date.set_text(time.strftime("%d/%b/%Y", tm)) + + def fill(self, ev): + if self.full_label.get_text() == "(full)": + self.full_label.set_text("(not full)") + self.fill_button.child.set_text("Fill") + else: + self.full_label.set_text("(full)") + self.fill_button.child.set_text("non-Fill") + + + def discard(self, ev): + self.listui.show() + self.editui.hide() + pass + + def save(self, ev): + self.listui.show() + self.editui.hide() + date = time.strftime('%Y-%m-%d', self.date_get()) + km = "%d" % self.check_entry(self.km_entry) + ltr = "%.2f" % self.check_entry(self.l_entry) + full = 'full' if self.full_label.get_text() == "(full)" else 'notfull' + price = "%.2f" % self.check_entry(self.cost_entry) + if self.curr == None: + self.list.append([date,km,ltr,full,price]) + else: + self.list[self.curr] = [date,km,ltr,full,price] + self.list.sort(reverse=True) + self.pl.set_list(self.list) + self.listui.list.list_changed() + if self.curr == None: + self.listui.list.select(0) + self.curr = 0 + else: + self.listui.list.select(self.curr) + self.save_file() + self.selected(None, self.curr) + + +if __name__ == "__main__": + + p = Petrol("/data/RAV4") + p.set_default_size(480, 640) + p.show() + gtk.main() + diff --git a/plato/cmd.py b/plato/cmd.py new file mode 100644 index 0000000..c841294 --- /dev/null +++ b/plato/cmd.py @@ -0,0 +1,215 @@ +# +# Support for running commands from the Laucher +# +# Once a command is run, we watch for it to finish +# and update status when it does. +# We also maintain window list and for commands that appear +# in windows, we associate the window with the command. +# ShellTask() runs a command and captures output in a text buffer +# that can be displayed in a FingerScroll +# WinTask() runs a command in a window + +import os,fcntl, gobject +import pango +from subprocess import Popen, PIPE +from fingerscroll import FingerScroll + +class ShellTask: + # Normally there is one button : "Run" + # When this is pressed we create a 'FingerScroll' text buffer + # to hold the output. + # The button then becomes 'ReRun' + # When we get deselected, the buffer gets hidden and we get + # new button 'Display' + def __init__(self, name, line, owner): + self.format = "cmd" + self.append = False + self.is_full = True + # remove leading '!' + line = line[1:] + if line[0] == '-': + self.is_full = False + line = line[1:] + if line[0] == '+': + self.append = True + line = line[1:] + if name == None: + self.name = line + else: + self.name = name + self.cmd = line + self.buffer = None + self.displayed = False + self.job = None + self.owner = owner + + def buttons(self): + if self.displayed: + if self.job: + return ['-','Kill'] + else: + return ['ReRun', 'Close'] + if self.buffer != None: + if self.job: + return ['-', 'Display'] + else: + return ['Run', 'Display'] + return ['Run'] + + def embedded(self): + if self.displayed: + return self.buffer + return None + def embed_full(self): + return self.is_full + + def press(self, num): + if num == 1: + if self.displayed and self.job: + # must be a 'kill' request' + os.kill(self.job.pid, 15) + return + self.displayed = not self.displayed + self.owner.update(self, False) + return + + if self.job: + return + + if self.buffer == None: + self.buffer = FingerScroll() + self.buffer.show() + self.buffer.connect('hide', self.unmap_buff) + + fd = pango.FontDescription('Monospace 10') + fd.set_absolute_size(15*pango.SCALE) + self.buffer.modify_font(fd) + self.buff = self.buffer.get_buffer() + if not self.append: + self.buff.delete(self.buff.get_start_iter(), self.buff.get_end_iter()) + # run the program + self.job = Popen(self.cmd, shell=True, close_fds = True, + stdout=PIPE, stderr=PIPE) + + def set_noblock(f): + flg = fcntl.fcntl(f, fcntl.F_GETFL, 0) + fcntl.fcntl(f, fcntl.F_SETFL, flg | os.O_NONBLOCK) + set_noblock(self.job.stdout) + set_noblock(self.job.stderr) + self.wc = gobject.child_watch_add(self.job.pid, self.done) + self.wout = gobject.io_add_watch(self.job.stdout, gobject.IO_IN, self.read) + self.werr = gobject.io_add_watch(self.job.stderr, gobject.IO_IN, self.read) + + self.displayed = True + + def read(self, f, dir): + l = f.read() + self.buff.insert(self.buff.get_end_iter(), l) + gobject.idle_add(self.adjust) + if l == "": + return False + return True + + def adjust(self): + adj = self.buffer.vadj + adj.set_value(adj.upper - adj.page_size) + + def done(self, *a): + self.read(self.job.stdout, None) + self.read(self.job.stderr, None) + gobject.source_remove(self.wout) + gobject.source_remove(self.werr) + self.job.stdout.close() + self.job.stderr.close() + self.job = None + self.owner.update(self, None) + + def unmap_buff(self, widget): + if self.job == None: + self.displayed = False + + +class WinTask: + # A WinTask runs a command and expects it to + # create a window. This window can then be + # raised or closed, or the process killed. + # we find out about windows appearing + # by connecting to the 'new-window' signal in + # the owner + # When there is no process, button in "Run" + # When there is a process but no window, "Kill" + # When there is process and window, "Raise", "Close", "Kill" + def __init__(self, name, window, line, owner): + self.name = name + self.job = None + self.win_id = None + self.win = None + self.window_name = window + self.cmd = line + self.embedded = lambda:None + self.owner = owner + owner.connect('new-window', self.new_win) + owner.connect('lost-window', self.lost_win) + owner.connect('request-window', self.request_win) + + def buttons(self): + if not self.job: + return ['Run'] + if not self.win_id: + return ['-','Kill'] + return ['Raise','Close','Kill'] + + def get_format(self): + if self.job or self.win: + return "win" + else: + return "cmd" + + def press(self, num): + if not self.job and not self.win: + if num != 0: + return + self.job = Popen(self.cmd, shell=True, close_fds = True) + self.wc = gobject.child_watch_add(self.job.pid, self.done) + return + + if not self.win_id and self.job: + if num != 1: + return + os.kill(self.job.pid, 15) + return + + # We have a win_id and a job + if num == 0: + self.win.raise_win() + elif num == 1: + self.win.close_win() + elif num == 2 and self.job: + os.kill(self.job.pid, 15) + + def done(self, *a): + self.job = None + self.win_id = None + self.owner.update(self, None) + + def new_win(self, source, win): + if self.job and self.job.pid == win.pid: + self.win_id = win.id + self.win = win + if self.window_name and self.window_name == win.name: + self.win_id = win.id + self.win = win + if self.win_id: + self.format = "win" + self.owner.update(self, None) + + def lost_win(self, source, id): + if self.win_id == id: + self.win_id = None + self.win = None + self.format = "cmd" + self.owner.update(self, None) + + def request_win(self, source, name): + if self.window_name == name: + self.press(0) diff --git a/plato/grouptypes.py b/plato/grouptypes.py new file mode 100644 index 0000000..1ebff72 --- /dev/null +++ b/plato/grouptypes.py @@ -0,0 +1,105 @@ +# +# Generic code for group types +# +# A 'group' must provide: +# +# - 'parse' to take a line from the config file and interpret it. +# - 'name' and 'format' which are present to ListSelect. +# 'format' is normally 'group'. +# - 'tasks' which provides a task list to display in the other +# ListSelect. +# +# A 'group' can generate the signal: +# - 'new-task-list' to report that the task list has changed +# ??? +# + +import cmd, re + +class IgnoreType: + def __init__(self, win, name): + self.name = name + self.format = 'group' + self.tasks = [] + + def parse(self, line): + pass + +class ListType: + """A ListType group parses a list of tasks out of + the config file and provides them as a static list. + Tasks can be: + !command - run command and capture text output + (window)command - run command and expect it to appear as 'window' + command.command - run an internal command from the given module + In each case, arguments can follow and are treated as you would expect. + """ + + def __init__(self, win, name): + self.name = name + self.format = 'group' + self.tasks = [] + self.win = win + + def parse(self, line): + line = line.strip() + m = re.match('^([A-Za-z0-9_ ]+)=(.*)', line) + if m: + name = m.groups()[0].strip() + line = m.groups()[1].strip() + else: + name = None + if line[0] == '!': + self.parse_txt(name, line) + elif line[0] == '(': + self.parse_win(name, line) + else: + self.parse_internal(name, line) + + def parse_txt(self, name, line): + task = cmd.ShellTask(name, line, self) + if task.name: + self.tasks.append(task) + + def parse_win(self, name, line): + f = line[1:].split(')', 1) + if len(f) != 2: + return + if name == None: + name = f[0] + task = cmd.WinTask(name, f[0], f[1], self) + if task: + self.tasks.append(task) + + def parse_internal(self, name, line): + # split in to words, honouring quotes + words = map(lambda a: a[0].strip('"')+a[1].strip("'")+a[2], + re.findall('("[^"]*")?(\'[^\']*\')?([^ "\'][^ "\']*)? *', + line))[:-1] + + cmd = words[0].split('.', 1) + if len(cmd) == 1: + words[0] = "internal."+cmd[0] + cmd = ['internal',cmd[0]] + if len(cmd) != 2: + return + + exec "import plato_" + cmd[0] + fn = eval("plato_"+words[0]) + if name == None: + name = cmd[0] + task = fn(name, self, *words[1:]) + if task: + self.tasks.append(task) + + def update(self, task, refresh): + if refresh: + self.win.select(self, task) + self.win.update(task) + def emit(self, *args): + self.win.emit(*args) + + def connect(self, name, *cmd): + return self.win.connect(name, *cmd) + def queue_draw(self): + return self.win.queue_draw() diff --git a/plato/plato.py b/plato/plato.py new file mode 100644 index 0000000..de2293a --- /dev/null +++ b/plato/plato.py @@ -0,0 +1,564 @@ +#!/usr/bin/env python + +#TODO +# aux button +# calculator +# sms +# entry shows number of new messages +# embed shows latest new message if there is one +# buttons allow 'read' or 'open' +# phone calls +# number of missed calls, or incoming number +# embed shows call log, or tap board +# buttons allow 'call' or 'answer' or 'open' .... +# embed calendar +# selected by 'date' +# tapping can select a date +# run windowed program +# monitor list of windows +# address list +# this is a 'group' where 'tasks' are contacts + + +# Launcher, version 2. +# This module just defines the UI and plugin interface +# All tasks etc go in loaded modules. +# +# The elements of the UI are: +# +# ---------------------------------- +# | Text (Entry) box (one line) | +# +--------------------------------+ +# | | | +# | group | task | +# | selection | selection | +# | list | list | +# | | | +# | | | +# +----------------+ | +# | optional | | +# | task- | | +# | specific | | +# | widget +---------------| +# | | secondary | +# | | button row | +# +----------------+---------------+ +# | Main Button Row | +# +--------------------------------+ +# +# If the optional widget is present, then the Main Button Row +# disappears and the secondary button row is used instead. +# +# The selection lists auto-scroll when an item near top or bottom +# is selected. Selecting an entry only updates other parts of +# the UI and does not perform any significant action. +# Actions are performed by buttons which appear in a button +# row. +# The optional widget can be anything provided by the group +# or task, for example: +# - tap-board for entering text +# - calendar for selecting a date +# - display area for small messages (e.g. SMS) +# - preview during file selection +# - map of current location +# +# Entered text alway appears in the Text Entry box and +# is made available to the current group and task +# That object may use the text and may choose to delete it. +# e.g. +# A calculator task might display an evaluation of the text +# A call task might offer to call the displayed number, and +# delete it when the call completes +# An address-book group might restrict displayed tasks +# to those which match the text +# A 'todo' task might set the text to match the task content, +# then update the content as the text changes. +# +# A config file describes the groups. +# It is .platorc in $HOME +# A group is enclosed in [] and can have an optional type: +# [group1] +# [Windows/winlist] +# If no type is given the 'list' type is used. +# this parses following lines to create a static list of tasks. +# Other possible types are: +# winlist: list of windows on display +# contacts: list of contacts +# notifications: list of recent notifications (alarm/battery/whatever) +# +# the 'list' type is described in more detail in 'grouptypes'. +# + +import sys, gtk, os, pango +import gobject + +if __name__ == '__main__': + sys.path.insert(1, '/home/neilb/home/freerunner/lib') + sys.path.insert(1, '/home/neilb/home/freerunner/sms') + sys.path.insert(1, '/root/lib') + +from tapboard import TapBoard +import grouptypes, listselect, scrawl +from grouptypes import * +from window_group import * +from evdev import EvDev + +import plato_gsm + +class MakeList: + # This class is a wrapper on a 'tasklist' which presents an + # entry list that can be used by ListSelect. + # Each entry in the list is a name and a format + # The group can either contain a list of task: tasks + # or a function: get_task + # in the later case the length is reported as -1 + # + # a task must provide a .name or a get_name function + # similarly a .format or a .get_format function + def __init__(self, group): + self.group = group + + def __getitem__(self, ind): + i = self.get_item(ind) + if i == None: + return None + try: + name = i.name + except AttributeError: + name = i.get_name() + try: + format = i.format + except AttributeError: + format = i.get_format() + return (name, format) + + def __len__(self): + try: + e = self.group.tasks + l = len(e) + except AttributeError: + l = 50000 + return l + + def get_item(self, ind): + try: + e = self.group + if ind >= 0 and ind < len(e): + i = e[ind] + else: + i = None + except AttributeError: + try: + e = self.group.tasks + if ind >= 0 and ind < len(e): + i = e[ind] + else: + i = None + except AttributeError: + i = self.group.get_task(ind) + return i + + +class LaunchWin(gtk.Window): + __gsignals__ = { + 'new-window' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, + (gobject.TYPE_PYOBJECT,)), + 'lost-window' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, + (gobject.TYPE_INT,)), + 'request-window' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, + (gobject.TYPE_STRING,)), + } + def __init__(self): + gtk.Window.__init__(self, gtk.WINDOW_TOPLEVEL) + self.set_default_size(480, 640) + self.connect('destroy', lambda w: gtk.main_quit()) + + self.embeded_widget = None + self.embeded_full = False + self.hidden = True + self.next_group = 0 + self.widgets = {} + self.buttons = [] + self.create_ui() + self.load_config() + self.grouplist.select(0) + + def create_ui(self): + # Create the UI framework, first the components + + # The Entry + e1 = gtk.Entry() + e1.set_alignment(0.5) ; # Center text + e1.connect('changed', self.entry_changed) + e1.connect('backspace', self.entry_changed) + e1.show() + self.entry = e1 + + # Label for the entry + lbl = gtk.Label("Label") + lbl.hide() + self.label = lbl + + # The text row: Entry fills, label doesn't + tr = gtk.HBox() + tr.pack_start(lbl, expand = False) + tr.pack_end(e1, expand = True) + tr.show() + + # The group list + l1 = listselect.ListSelect(center = False, linescale = 1.3, markup = True) + l1.connect('selected', self.group_select) + # set appearance here + l1.set_colour('group','blue') + self.grouplist = l1 + l1.set_zoom(35) + l1.show() + + # The task list + l2 = listselect.ListSelect(center = True, linescale = 1.3, markup = True) + l2.connect('selected', self.task_select) + l2.set_colour('cmd', 'black') + l2.set_colour('win', 'blue') + l2.set_zoom(35) + # set appearance + self.tasklist = l2 + l2.show() + + # The embedded widget: provide a VBox as a place holder + v1 = gtk.VBox() + self.embed_box = v1 + + # The Main button box - buttons are added later + h1 = gtk.HBox(True) + self.main_buttons = h1 + # HACK + h1.set_size_request(-1, 80) + h1.show() + + # The Secondary button box + h2 = gtk.HBox(True) + h2.set_size_request(-1, 60) + self.secondary_buttons = h2 + + # Now make the two columns + + v2 = gtk.VBox(True) + v2.pack_start(self.grouplist, expand = True) + v2.pack_end(self.embed_box, expand = False) + v2.show() + + v3 = gtk.VBox() + v3.pack_start(self.tasklist, expand = True) + v3.pack_end(self.secondary_buttons, expand = False) + v3.show() + + # and bind them together + h3 = gtk.HBox(True) + h3.pack_start(v2, expand=True) + h3.pack_end(v3, expand=True) + h3.show() + + # A vbox to hold tr and main buttons + v3a = gtk.VBox() + v3a.show() + v3a.pack_start(tr, expand=False) + v3a.pack_end(h3, expand=True) + self.non_buttons = v3a + + # a box for a 'full screen' embedded widget + v3b = gtk.VBox() + v3b.hide() + self.embed_full_box = v3b + + + # And now one big vbox to hold it all + v4 = gtk.VBox() + v4.pack_start(self.non_buttons, expand=True) + v4.pack_start(self.embed_full_box, expand=True) + v4.pack_end(self.main_buttons, expand=False) + v4.show() + self.add(v4) + self.show() + + ## We want writing recognistion to work + ## over the whole middle section. Only that + ## turns out to be too hard for my lowly gtk + ## skills. So we do recognition separately + ## on each selection box only. + s1 = scrawl.Scrawl(l1, self.getsym, lambda p: l1.tap(p[0],p[1])) + s2 = scrawl.Scrawl(l2, self.getsym, lambda p: l2.tap(p[0],p[1])) + s1.set_colour('red') + s2.set_colour('red') + + + ctx = self.get_pango_context() + fd = ctx.get_font_description() + fd.set_absolute_size(30 * pango.SCALE) + self.button_font = fd; + self.entry.modify_font(fd) + self.label.modify_font(fd) + + def load_config(self): + fname = os.path.join(os.environ['HOME'], ".platorc") + types = {} + types['ignore'] = IgnoreType + types['list' ] = ListType + types['winlist'] = WindowType + types['call_list']=plato_gsm.call_list + groups = [] + f = open(fname) + gobj = None + for line in f: + l = line.strip() + if not l: + continue + if l[0] == '[': + l = l.strip('[]') + f = l.split('/', 1) + group = f[0] + if len(f) > 1: + group_type = f[1] + else: + group_type = "list" + if group_type in types: + gobj = types[group_type](self, group) + else: + gobj = types['ignore'](self, group) + groups.append(gobj) + elif gobj != None: + gobj.parse(l) + self.grouplist.list = MakeList(groups) + self.grouplist.list_changed() + + + def entry_changed(self, entry): + print "fixme", entry.get_text() + + def group_select(self, list, item): + g = list.list.get_item(item) + self.tasklist.list = MakeList(g) + self.tasklist.list_changed() + self.tasklist.select(None) + if self.tasklist.list != None: + self.task_select(self.tasklist, + self.tasklist.selected) + + def set_group(self, group): + self.tasklist.list = MakeList(group) + self.tasklist.list_changed() + self.tasklist.select(None) + if self.tasklist.list != None: + self.task_select(self.tasklist, + self.tasklist.selected) + + def task_select(self, list, item): + if item == None: + self.set_buttons(None) + self.set_embed(None, None) + else: + task = self.tasklist.list.get_item(item) + if task: + self.set_buttons(task.buttons()) + bed = task.embedded() + full = False + if bed != None and type(bed) != str: + full = task.embed_full() + self.set_embed(bed, full) + + def update(self, task): + if self.tasklist.selected != None and \ + self.tasklist.list.get_item(self.tasklist.selected) == task: + self.set_buttons(task.buttons()) + bed = task.embedded() + full = False + if bed != None and type(bed) != str: + full = task.embed_full() + self.set_embed(bed, full) + + def select(self, group, task): + i = 0 + gl = self.grouplist.list + while i < len(gl) and gl.get_item(i) != None: + if gl.get_item(i) == group: + self.grouplist.select(i) + break + i += 1 + tl = self.tasklist.list + i = 0 + while i < len(tl) and tl.get_item(i) != None: + if tl.get_item(i) == task: + self.tasklist.select(i) + self.present() + break + i += 1 + + def set_buttons(self, list): + if not list: + # hide the button boxes + self.secondary_buttons.hide() + self.main_buttons.hide() + self.buttons = [] + return + if self.same_buttons(list): + return + + self.buttons = list + self.update_buttons(self.main_buttons) + self.update_buttons(self.secondary_buttons) + if self.embeded_widget and not self.embeded_full: + self.main_buttons.hide() + self.secondary_buttons.show() + else: + self.secondary_buttons.hide() + self.main_buttons.show() + + def same_buttons(self, list): + if len(list) != len(self.buttons): + return False + for i in range(len(list)): + if list[i] != self.buttons[i]: + return False + return True + + def update_buttons(self, box): + # make sure there are enough buttons + have = len(box.get_children()) + need = len(self.buttons) - have + if need > 0: + for i in range(need): + b = gtk.Button("?") + b.child.modify_font(self.button_font) + b.set_property('can-focus', False) + box.add(b) + b.connect('clicked', self.button_pressed, have + i) + have += need + b = box.get_children() + # hide extra buttons + if need < 0: + for i in range(-need): + b[have-i-1].hide() + # update each button + for i in range(len(self.buttons)): + b[i].child.set_text(self.buttons[i]) + b[i].show() + + def button_pressed(self, widget, num): + self.hidden = True + self.tasklist.list.get_item(self.tasklist.selected).press(num) + self.task_select(self.tasklist, + self.tasklist.selected) + + def set_embed(self, widget, full): + if type(widget) == str: + widget, full = self.make_widget(widget) + + if widget == self.embeded_widget and full == self.embeded_full: + return + if self.embeded_widget: + self.embeded_widget.hide() + if self.embeded_full: + self.embed_full_box.remove(self.embeded_widget) + else: + self.embed_box.remove(self.embeded_widget) + self.embeded_widget = None + self.embeded_widget = widget + self.embeded_full = full + if widget and not full: + self.embed_box.add(widget) + widget.show() + self.main_buttons.hide() + self.embed_box.show() + self.non_buttons.show() + self.embed_full_box.hide() + if self.buttons: + self.secondary_buttons.show() + elif widget: + self.embed_full_box.add(widget) + widget.show() + self.embed_full_box.show() + self.non_buttons.hide() + if self.buttons: + self.main_buttons.show() + else: + self.embed_box.hide() + self.embed_full_box.hide() + self.non_buttons.show() + self.secondary_buttons.hide() + if self.buttons: + self.main_buttons.show() + + def make_widget(self, name): + if name in self.widgets: + return self.widgets[name] + if name == "tapboard": + w = TapBoard() + def key(w, k): + if k == '\b': + self.entry.emit('backspace') + elif k == 'Return': + self.entry.emit('activate') + elif len(k) == 1: + self.entry.emit('insert-at-cursor', k) + w.connect('key', key) + self.widgets[name] = (w, False) + return (w, False) + return None + + def getsym(self, sym): + print "gotsym", sym + if sym == '': + self.entry.emit('backspace') + elif sym == '': + self.entry.emit('activate') + elif len(sym) == 1: + self.entry.emit('insert-at-cursor', sym) + + def activate(self, *a): + # someone pressed a button or icon to get our + # attention. + # We raise the window and if nothing has happened recently, + # select first group + if self.hidden: + self.hidden = False + self.next_group = 0 + else: + if (self.next_group > len(self.grouplist.list) or + self.grouplist.list[self.next_group] == None): + self.next_group = 0 + self.grouplist.select(self.next_group) + self.next_group += 1 + + self.present() + + def ev_activate(self, down, moment, typ, code, value): + if typ != 1: + # not a key press + return + if code != 169 and code != 116: + # not AUX or Power + return + if value == 0: + # down press, wait for up + self.down_at = moment + return + self.activate() + +class LaunchIcon(gtk.StatusIcon): + def __init__(self, callback): + gtk.StatusIcon.__init__(self) + self.set_from_stock(gtk.STOCK_EXECUTE) + self.connect('activate', callback) + + +if __name__ == '__main__': + sys.path.insert(1, '/home/neilb/home/freerunner/lib') + sys.path.insert(1, '/root/lib') + l = LaunchWin() + i = LaunchIcon(l.activate) + try: + aux = EvDev("/dev/input/aux", l.ev_activate) + except OSError: + aux = None + + gtk.settings_get_default().set_long_property("gtk-cursor-blink", 0, "main") + gtk.main() diff --git a/plato/plato_gsm.py b/plato/plato_gsm.py new file mode 100644 index 0000000..dd3a88b --- /dev/null +++ b/plato/plato_gsm.py @@ -0,0 +1,484 @@ + +# +# plato plugins for mobile phone functionality. +# carrier: +# displays current carrier +# button will search for possible carriers +# while search, button show how long search has progressed +# on succcess, get one button per carrier valid for 2 minutes +# When one is selected, write that to /var/run/gsm/request_carrier +# +# calls: +# display incoming phone number and when there is one, allows +# 'answer' or 'reject', then '-' or 'hangup' +# When no call, buttons for 'dial' or 'contacts' +# +# newmsg: +# displays new message and allows more to be shown in embedded +# also allows sms reader to be run + +from plato_internal import file +from subprocess import Popen, PIPE +import os.path, fcntl, time, gobject, re, gtk + +def record(key, value): + f = open('/var/run/gsm-state/.new.' + key, 'w') + f.write(value) + f.close() + os.rename('/var/run/gsm-state/.new.' + key, + '/var/run/gsm-state/' + key) + + +def recall(key): + try: + fd = open("/var/run/gsm-state/" + key) + l = fd.read(1000) + fd.close() + except IOError: + l = "" + return l.strip() + +class carrier(file): + def __init__(self, name, owner): + file.__init__(self, name, owner, "/var/run/gsm-state/carrier") + self.buttons = self.cbuttons + + self.carriers = [] + self.carrier_buttons = None + self.carrier_valid = 0 + self.reader = None + self.job = None + self.job_start = 0 + self.timeout = None + + def cbuttons(self): + if self.job: + # waiting for carriers - can cancel + if self.timeout == None: + self.timeout = gobject.timeout_add(3000, self.change_button) + waited = time.time() - self.job_start + return [ 'Cancel after %d seconds' % waited] + + if self.carrier_buttons and time.time() - self.carrier_valid < 120: + return self.carrier_buttons + self.carrier_buttons = None + if (os.path.exists('/var/run/gsm-state/request_carrier') and + os.path.getsize('/var/run/gsm-state/request_carrier') > 1): + return ['Search Carriers','Cancel Request'] + return ['Search Carriers'] + + def change_button(self): + self.timeout = None + self.owner.update(self, None) + + def press(self, ind): + if self.job: + if ind == 0: + self.job.terminate() + return + + if self.carriers and self.carrier_buttons: + if ind < len(self.carriers): + num = self.carriers[ind][0] + record('request_carrier',num) + self.carrier_buttons = None + return + + if ind == 1: + record('request_carrier', '') + return + if ind != 0: + return + + # initiate search + # run gsm-carriers expecting output like: + # 50503 0 "voda AU" "vodafone AU" + # record output in carriers[] and short names in carrer_buttons + self.job = Popen("gsm-carriers", shell=True, close_fds=True, + stdout = PIPE) + self.job_start = time.time() + gobject.child_watch_add(self.job.pid, self.done) + flg = fcntl.fcntl(self.job.stdout, fcntl.F_GETFL, 0) + fcntl.fcntl(self.job.stdout, fcntl.F_SETFL, flg | os.O_NONBLOCK) + self.buf = '' + self.reader = gobject.io_add_watch(self.job.stdout, gobject.IO_IN, + self.read) + + def read(self, f, dir): + try: + l = f.read() + except IOError: + l = None + if l == None: + if self.job.poll() == None: + return True + elif l != '': + self.buf += l + return True + self.job.wait() + lines = self.buf.split('\n') + c=[] + b=[] + for line in lines: + words = re.findall('([^"][^ ]*|"[^"]*["]) *', line) + if len(words) == 4: + c.append(words) + b.append(words[2].strip('"')) + self.carriers = c + self.carrier_buttons = b + self.carrier_valid = time.time() + self.job = None + self.owner.update(self, None) + return False + + def done(self, *a): + if not self.job: + return + self.job.wait() + #flg = fcntl.fcntl(self.job.stdout, fcntl.F_GETFL, 0) + #fcntl.fcntl(self.job.stdout, fcntl.F_SETFL, flg & ~os.O_NONBLOCK) + self.read(self.job.stdout, None) + + + +class calls(file): + # Monitor 'status' which can be + # INCOMING BUSY on-call or empty + # INCOMING: + # get caller from 'incoming' and display - via contacts if possibe + # Buttons are 'answer','reject' + # BUSY: + # display BUSY + # Buttons are '-', 'cancel' + # on-call: + # Buttons are 'hold', 'cancel' + # empty: + # display (call logs) + # Buttons are "Received" "Dialled" "Dialer" "Contacts" + def __init__(self, name, owner): + file.__init__(self, name, owner, '/var/run/gsm-state/status') + self.buttons = self.cbuttons + self.get_name = self.cgetname + self.status = '' + self.choose_logs = False + self.caller = None + self.dialer_win = None + self.contacts_win = None + self.dj = None + self.cj = None + # catch queue_draw from 'file' + self.owner = self + self.gowner = owner + owner.connect('new-window', self.new_win) + owner.connect('lost-window', self.lost_win) + + def cbuttons(self): + if self.status == '' or self.status == '-': + if self.choose_logs: + return ['Call', "Recv'd\nCalls","Dialed\nCalls"] + else: + return ['Logs', "Dialer", "Contacts"] + if self.status == 'INCOMING': + return ['Answer','Reject'] + if self.status == 'BUSY': + return ['-','Cancel'] + if self.status == 'on-call': + return ['Hold','Cancel'] + return [] + + def cgetname(self): + status = file.get_name(self) + if self.status != status: + self.status = status + print "update for", status + # was "status == 'INCOMING'" to raise on incoming calls + # but want Dailer to do that + self.gowner.update(self, False) + if self.status == 'INCOMING': + if self.caller == None: + self.caller = recall('incoming') + if self.caller == '': + self.caller = '(private)' + else: + contacts = contactdb.contacts() + n = contacts.find_num(self.caller) + if n: + self.caller = n.name + else: + self.caller = None + if self.status == '' or self.status == '-': + if self.choose_logs: + return '(call logs)' + else: + return '(no call)' + if self.status == 'INCOMING': + return self.caller + ' calling' + if self.status == 'BUSY': + return 'BUSY' + if self.status == 'on-call': + return "On Call" + return '??'+self.status + + def press(self, ind): + if self.status == '' or self.status == '-': + if self.choose_logs: + if ind == 0: + self.choose_logs = False + elif ind == 1: + self.gowner.win.set_group(call_list(self.gowner.win, '', True)) + elif ind == 2: + self.gowner.win.set_group(call_list(self.gowner.win, '', False)) + else: + if ind == 0: + self.choose_logs = True + elif ind == 1: + if self.dialer_win: + self.dialer_win.raise_win() + elif not self.dj: + self.dj = Popen("dialer", shell=True, close_fds = True) + gobject.child_watch_add(self.dj.pid, self.djdone) + elif ind == 2: + if self.contacts_win: + self.contacts_win.raise_win() + elif not self.cj: + self.cj = Popen("contacts", shell=True, close_fds = True) + gobject.child_watch_add(self.cj.pid, self.cjdone) + self.gowner.update(self, None) + return + + if self.status == 'INCOMING': + if ind == 0: + record('call','answer') + elif ind == 1: + record('call','') + return + + if ind == 0: + print "on hold??" + return + if ind == 1: + record('call','') + + def queue_draw(self): + # file changed + self.cgetname() + self.gowner.queue_draw() + + def djdone(self, *a): + self.dj.wait() + self.dj = None + def cjdone(self, *a): + self.cj.wait() + self.cj = None + + def new_win(self, source, win): + if win.name == "Dialer": + self.dialer_win = win + if win.name == "Contacts": + self.contacts_win = win + + def lost_win(self, source, id): + if self.dialer_win and self.dialer_win.id == id: + self.dialer_win = None + if self.contacts_win and self.contacts_win.id == id: + self.contacts_win = None + +import contactdb + +contacts = None + +def friendly_time(tm, secs): + now = time.time() + if secs > now or secs < now - 7*24*3600: + return time.strftime("%Y-%m-%d %H:%M:%S", tm) + age = now - secs + if age < 12*3600: + return time.strftime("%H:%M:%S", tm) + return time.strftime("%a %H:%M:%S", tm) + +class corresp: + # This task represents a correspondant in the + # lists of dialled numbers and received calls + # It displays the contact name/number and time/date + # in a friendly form + # If there is a number we provide a button to call-back + # and one to find contact + def __init__(self, name, owner, when): + global contacts + self.format="brown" + self.owner = owner + self.number = name + self.embedded = lambda:None + dt, tm = when + self.when = time.strptime(dt+'='+tm, '%Y-%m-%d=%H:%M:%S') + self.secs = time.mktime(self.when) + + if contacts == None: + contacts = contactdb.contacts() + if name == None: + self.contact = "-private-number-" + else: + n = contacts.find_num(name) + if n: + self.contact = n.name + else: + self.contact = name + + def get_name(self): + w = friendly_time(self.when, self.secs) + return '%s\n%s' % ( + self.contact, w) + + def buttons(self): + if self.number: + return ['Call','Txt','Contacts'] + return [] + + def press(self, ind): + if ind == 0: + record('call',self.number) + send_number('voice-dial', self.number) + self.owner.emit('request-window','Dialer') + if ind == 1: + send_number('sms-new', self.number) + self.owner.emit('request-window','SendSMS') + if ind == 2: + send_number('contact-find', self.number) + self.owner.emit('request-window','Contacts') + +sel_number = None +clips = {} +def clip_get_data(clip, sel, info, data): + global sel_number + sel.set_text(sel_number) +def clip_clear_data(clip, data): + global sel_number + sel_number = None + c.set_with_data([(gtk.gdk.SELECTION_TYPE_STRING, 0, 0)], + clip_get_data, clip_clear_data, None) + +def send_number(sel, num): + global sel_number, clips + sel_number = num + if sel not in clips: + clips[sel] = gtk.Clipboard(selection = sel) + c = clips[sel] + c.set_with_data([(gtk.gdk.SELECTION_TYPE_STRING, 0, 0)], + clip_get_data, clip_clear_data, None) + + + +class call_list: + # A list of 'corresp' tasks taken from /var/log/incoming or + # /var/log/outgoing + def __init__(self, win, name, incoming = None): + self.win = win + if incoming == None: + incoming = name == 'incoming' + + if incoming: + self.list = incoming_list(self.update) + self.name = "Incoming Calls" + else: + self.list = outgoing_list(self.update) + self.name = "Outgoing Calls" + self.format = 'group' + + def get_task(self, ind): + if ind >= len(self.list): + return None + start, end, num = self.list[ind] + return corresp(num, self, start) + + def update(self): + self.win.queue_draw() + def emit(self, *a): + self.win.emit(*a) + + +import dnotify +class incoming_list: + # present as a list of received calls + # and notify whenever it changes + # Each entry is a triple (start, end, number) + # start and end are (date, time) + def __init__(self, notify, file='incoming'): + self.list = [] + self.notify = notify + self.watch = dnotify.dir("/var/log") + self.fwatch = self.watch.watch(file, self.changed) + self.changed() + def __len__(self): + return len(self.list) + def __getitem__(self, ind): + return self.list[-1-ind] + def flush_one(self, start, end, number): + if start: + self.lst.append((start, end, number)) + def changed(self): + self.lst = [] + + try: + f = open("/var/log/incoming") + except IOError: + f = [] + start = None; end=None; number=None + for l in f: + w = l.split() + if len(w) != 3: + continue + if w[2] == '-call-': + self.flush_one(start, end, number) + start = (w[0], w[1]) + number = None; end = None + elif w[2] == '-end-': + end = (w[0], w[1]) + self.flush_one(start, end, number) + start = None; end = None; number = None + else: + number = w[2] + if not start: + start = (w[0], w[1]) + if number: + self.flush_one(start, end, number) + try: + f.close() + except AttributeError: + pass + self.list = self.lst + self.notify() + +class outgoing_list(incoming_list): + # present similar list for outgoing calls + def __init__(self, notify): + incoming_list.__init__(self, notify, file='outgoing') + + def changed(self): + self.lst = [] + try: + f = open("/var/log/outgoing") + except IOError: + f = [] + start = None; end=None; number=None + for l in f: + w = l.split() + if len(w) != 3: + continue + if w[2] == '-end-': + end = (w[0], w[1]) + self.flush_one(start, end, number) + start = None; end = None; number = None + else: + self.flush_one(start, end, number) + start = (w[0], w[1]) + number = w[2] + if number: + self.flush_one(start, end, number) + try: + f.close() + except AttributeError: + pass + self.list = self.lst + self.notify() + + diff --git a/plato/plato_internal.py b/plato/plato_internal.py new file mode 100644 index 0000000..2e50cef --- /dev/null +++ b/plato/plato_internal.py @@ -0,0 +1,176 @@ +# +# internal commands for 'plato' +# some of these affect plato directly, some are just +# ad hoc simple things. + +import time as _time +import gobject, gtk +import dnotify, os + +class date: + def __init__(self, name, owner): + self.buttons = lambda:['Calendar'] + self.embedded = lambda:None + self.format = 'cmd' + self.timeout = None + self.owner = owner + + def press(self, ind): + if ind != 0: + return + self.gowner.emit('request-window','cal') + + + def get_name(self): + now = _time.time() + if self.timeout == None: + next_hour = int(now/60/60)+1 + self.timeout = gobject.timeout_add( + int (((next_hour*3600) - now) * 1000), + self.do_change) + return _time.strftime('%d-%b-%Y', + _time.localtime(now)) + + def do_change(self): + self.timeout = None + if self.owner: + self.owner.queue_draw() + + +class time: + def __init__(self, name, owner): + self.buttons = lambda:None + self.embedded = lambda:None + self.format = 'cmd' + self.timeout = None + self.owner = owner + + def get_name(self): + now = _time.time() + if self.timeout == None: + next_min = int(now/60)+1 + self.timeout = gobject.timeout_add( + int (((next_min*60) - now) * 1000), + self.do_change) + return _time.strftime('%H:%M', + _time.localtime(_time.time())) + + def do_change(self): + self.timeout = None + if self.owner: + self.owner.queue_draw() + +zonelist = None +def get_zone(ind): + global zonelist + if zonelist == None: + try: + f = open("/data/timezone_list") + l = f.readlines() + except IOError: + l = [] + zonelist = map(lambda x:x.strip(), l) + if ind < 0 or ind >= len(zonelist): + return "UTC" + return zonelist[ind] + +class tz: + # Arg is either a timezone name or a number + # to index into a file containing a list of + # timezone names '0' is first line + def __init__(self, name, owner, zone): + try: + ind = int(zone) + self.zone = get_zone(ind) + except ValueError: + self.zone = zone + self.buttons = lambda:None + self.embedded = lambda:None + self.format = 'darkgreen' + self.owner = owner + + def get_name(self): + if 'TZ' in os.environ: + TZ = os.environ['TZ'] + else: + TZ = None + os.environ['TZ'] = self.zone + _time.tzset() + now = _time.time() + tm = _time.strftime("%d-%b-%Y %H:%M", _time.localtime(now)) + + if TZ: + os.environ['TZ'] = TZ + else: + del(os.environ['TZ']) + _time.tzset() + return ''+tm+"\n"+self.zone+'' + + +class file: + def __init__(self, name, owner, path): + self.buttons = lambda:None + self.embedded = lambda:None + self.path = path + self.format = 'cmd' + self.owner = owner + self.watch = None + self.fwatch = None + + def get_name(self): + try: + f = open(self.path) + l = f.readline().strip() + f.close() + except IOError: + l = "-" + self.set_watch() + + return l + + def set_watch(self): + if self.watch == None: + try: + self.watch = dnotify.dir(os.path.dirname(self.path)) + except OSError: + self.watch = None + if self.watch == None: + return + if self.fwatch == None: + self.fwatch = self.watch.watch(os.path.basename(self.path), + self.file_changed) + + def file_changed(self, thing): + if self.fwatch: + self.fwatch.cancel() + self.fwatch = None + if self.owner: + self.owner.queue_draw() + +class quit: + def __init__(self, name, owner): + self.buttons = lambda:['Exit'] + self.embedded = lambda:None + self.name = name + self.format = 'cmd' + + def press(self, ind): + gtk.main_quit() + +class zoom: + def __init__(self, name, owner): + self.buttons = lambda:['group+','group-','task+','task-'] + self.owner = owner + self.name = name + self.format = 'cmd' + self.embedded = lambda:None + def press(self, ind): + w = self.owner.win + if ind == 0: + w.grouplist.set_zoom(w.grouplist.zoom+1) + if ind == 1: + w.grouplist.set_zoom(w.grouplist.zoom-1) + if ind == 2: + w.tasklist.set_zoom(w.tasklist.zoom+1) + if ind == 3: + w.tasklist.set_zoom(w.tasklist.zoom-1) diff --git a/plato/plato_settings.py b/plato/plato_settings.py new file mode 100644 index 0000000..4a1e819 --- /dev/null +++ b/plato/plato_settings.py @@ -0,0 +1,34 @@ +import os, stat + +class alert: + def __init__(self, name, owner): + blist = [] + for i in os.listdir("/etc/alert"): + if stat.S_ISDIR(os.lstat("/etc/alert/"+i)[0]): + blist.append(i) + self.blist = blist + + self.embedded = lambda:None + self.format = 'cmd' + self.timeout = None + self.owner = owner + try: + self.mode = os.readlink("/etc/alert/normal") + except: + self.mode = '??' + + def buttons(self): + return self.blist + + def press(self, ind): + if ind < 0 or ind >= len(self.blist): + return + o = self.blist[ind] + os.unlink("/etc/alert/normal") + os.symlink(o, "/etc/alert/normal") + self.mode = o + self.owner.queue_draw() + + def get_name(self): + return 'mode: ' + self.mode + diff --git a/plato/plato_sms.py b/plato/plato_sms.py new file mode 100644 index 0000000..05ce4ba --- /dev/null +++ b/plato/plato_sms.py @@ -0,0 +1,89 @@ +# +# plato plugin for text messages +# +# + +import gtk +from plato_internal import file +from fingerscroll import FingerScroll +from storesms import SMSstore + +import contactdb +contacts = None + +def protect(txt): + txt = txt.replace('&', '&') + txt = txt.replace('<', '<') + txt = txt.replace('>', '>') + return txt + +class newmsg(file): + # display either (SMS) or recipient of last message + # button is "open" to load SendSMS + # embedded is most recent text message + def __init__(self, name, owner): + file.__init__(self, name, owner, "/data/SMS/newmesg") + self.owner = self + self.gowner = owner + self.get_name = self.mgetname + self.buttons = self.mbuttons + self.embedded = self.membedded + self.messages = 0 + self.who_from = None + self.buffer = FingerScroll(gtk.WRAP_WORD_CHAR) + self.buffer.show() + self.buff = self.buffer.get_buffer() + self.store = SMSstore("/data/SMS") + self.last_txt = 0 + global contacts + if contacts == None: + contacts = contactdb.contacts() + self.queue_draw() + + def mgetname(self): + if self.messages == 0: + return '(SMS)' + if self.messages == 1: + return self.who_from + return '%s (+%d)' % (self.who_from, self.messages-1) + + def mbuttons(self): + return ['Open'] + + def membedded(self): + if self.messages == 0: + return None + return self.buffer + def embed_full(self): + return False + + def press(self, ind): + self.gowner.emit('request-window','SendSMS') + + def set_display(self, mesg): + self.buff.delete(self.buff.get_start_iter(), + self.buff.get_end_iter()) + self.buff.insert(self.buff.get_end_iter(), mesg) + + def queue_draw(self): + # newmesg has changed + # need to get new messages and set + # embedded, message, and from + # + (next, l) = self.store.lookup(None, 'NEW') + self.messages = len(l) + if len(l) >= 1: + self.set_display(l[0].text) + self.who_from = l[0].correspondent + global contacts + if self.who_from: + c = contacts.find_num(self.who_from) + if c: + self.who_from = c.name + + if l[0].stamp > self.last_txt: + self.last_txt = l[0].stamp + self.gowner.update(self, True) + self.gowner.update(self, False) + self.gowner.queue_draw() + file.set_watch(self) diff --git a/plato/window_group.py b/plato/window_group.py new file mode 100644 index 0000000..fb55295 --- /dev/null +++ b/plato/window_group.py @@ -0,0 +1,66 @@ +# +# A plato 'group' of windows. +# This needs to be present for any window management +# to work. i.e. a WinTask won't see the window unless this +# was been activated. + +from wmctrl import * +import gobject + +class Wwrap: + # Wrap a window from wmctrl as a Task + def __init__(self, w): + self.w = w + self.name = w.name + self.format = "blue" + self.embedded = lambda:None + + def buttons(self): + if self.w.pid > 0: + return ['Raise', 'Close', 'Kill'] + return ['Raise','Close'] + + def press(self, ind): + if ind == 0: + self.w.raise_win() + return + if ind == 1: + self.w.close_win() + return + if ind == 2: + if self.w.pid > 1: + os.kill(self.w.pid, 15) + return + +class WindowType: + def __init__(self, win, name): + self.owner = win + self.format = 'group' + self.name = name + self.buttons = lambda:None + self.embedded = lambda:None + self.ignore = [] + self.list = winlist(add_handle = self.add) + self.current = {} + gobject.io_add_watch(self.list.fd, gobject.IO_IN, self.list.events) + self.list.on_change(self.change, self.add, self.delete) + + def parse(self, line): + # any window names listed in config file are ignored + self.ignore.append(line) + + def get_task(self, ind): + w = self.list.winfo + if ind >= len(w): + return None + return Wwrap(w[self.list.windows[ind]]) + + def change(self): + self.owner.queue_draw() + + def add(self, window): + print "emit new window", window.name + self.owner.emit('new-window',window) + + def delete(self, wid): + self.owner.emit('lost-window',wid) diff --git a/scribble/Sample-Pages/1 b/scribble/Sample-Pages/1 new file mode 100755 index 0000000..aea83a1 --- /dev/null +++ b/scribble/Sample-Pages/1 @@ -0,0 +1,19 @@ +"black":64,81:"Welcome to" +"black":72,159:62,162:45,170:34,178:36,192:48,201:63,207:79,214:92,226:97,245:95,263:83,278:65,287:51,289:50,278 +"black":146,200:136,204:122,212:120,241:131,246:144,249:159,243:169,235 +"black":177,206:188,221:193,231:186,211:186,199:199,187:209,183:219,182:234,181:238,191 +"black":255,186:261,196:270,213 +"black":263,111:270,142:276,170:280,186:284,199:286,209:292,224:292,213:293,203:294,192:299,178:317,187:317,201:307,210:292,202 +"black":321,103:321,119:327,156:330,176:332,193:333,207:337,194:345,175:357,171:365,182:360,197:333,194 +"black":371,96:371,108:374,124:376,138:380,154:382,171:387,195 +"black":416,182:421,171:433,161:429,151:417,155:407,168:405,183:410,193:422,196:454,183 +"red":433,211:423,212:405,214:393,215:380,218:367,221:355,225:344,230:332,233:319,237:307,241:295,245:283,250:258,258:235,264:224,267:214,270:201,273:189,276:177,279:153,285:142,289:129,293:117,296:106,300:96,303:79,310:63,316 +"black":92,384:91,396:92,417:91,429:89,417:88,400:90,385:94,371:102,360:113,357:123,359:131,371:129,383:123,394:112,402:100,403 +"black":133,403:139,414:137,401:148,390:162,390:173,395 +"black":174,411:189,407:200,404:208,394:198,388:186,391:178,403:182,418:195,426:208,426:221,422 +"black":253,398:240,394:227,396:232,406:243,410:225,421 +"black":288,405:264,404:275,413:286,418:276,428:259,431:249,431 +"red":359,349:370,359:381,370:391,377:404,391:399,401:389,407:379,411:366,418:356,425:346,431 +"red":94,506:"to continue" +"red":65,48:71,36:70,23:69,12:59,23 +"red":68,7:76,17:85,29 diff --git a/scribble/Sample-Pages/2 b/scribble/Sample-Pages/2 new file mode 100755 index 0000000..bc60b71 --- /dev/null +++ b/scribble/Sample-Pages/2 @@ -0,0 +1,23 @@ +"black":31,42:"Here is a page number" +"black":350,36:361,38:372,40:383,36:395,35:405,33:416,29:426,25:441,18:450,7:440,8:451,7:454,17:453,27 +"black":33,103:"Use" +"black":202,82:192,80:180,90:168,101:158,106:171,118:189,129:199,135 +"black":241,113:"To go back" +"black":92,161:106,168:116,174:128,183:140,193:134,204:124,214:113,225:103,234:93,242 +"black":176,198:"for next" +"black":58,259:54,274:58,291:65,303:79,308:91,303:101,264:101,254 +"black":112,276:117,289:120,300:124,289:138,284:149,295 +"black":192,283:182,282:172,285:170,296:180,298:190,290:192,279:191,265:186,246:183,234:186,251:189,262:190,273:192,284:194,294:197,304 +"black":217,291:226,280:236,278:247,287:245,299:226,305:213,294:215,284:225,280:241,280 +"black":58,349:67,359:71,371:70,360:71,350:77,338:90,335:102,337:112,342 +"black":123,352:134,355:146,355:156,354:163,343:151,338:135,340:129,356:141,371:167,375:184,372 +"black":224,336:222,346:208,350:201,364:208,376:218,372:225,357:225,337:222,321:224,342:227,353:230,363:235,373 +"black":261,361:262,349:274,343:285,348:288,359:272,374:258,369:258,359:264,349 +"black":65,406:68,395:69,409:71,425:73,436 +"black":86,412:76,414:58,416:46,416 +"black":110,436:"add page" +"black":96,499:84,499:73,500:63,500:53,502 +"black":121,510:"remove page" +"black":365,503:375,500:378,510:368,514:360,504:371,503 +"black":402,510:402,500:408,510:397,515:390,504:407,499 +"black":429,507:440,504:444,516:430,515:422,503:435,502 diff --git a/scribble/Sample-Pages/3 b/scribble/Sample-Pages/3 new file mode 100755 index 0000000..e3ddef8 --- /dev/null +++ b/scribble/Sample-Pages/3 @@ -0,0 +1,18 @@ +"black":117,45:122,57:108,51:96,48:82,53:70,62:57,74:47,86:41,100:40,117:41,128:48,146:56,156:66,165:78,171:110,177:133,169 +"black":158,119:"Clear page" +"black":195,159:"Before removal" +"black":83,235:84,224:80,250:79,270:78,289:78,319:79,330 +"black":165,229:149,223:128,220:105,218:60,213:46,213:36,213 +"black":120,308:130,303:143,298:156,306:154,320:139,329:129,326:127,308 +"black":186,297:175,305:168,315:178,318:189,315:199,304:201,321:203,332:169,367:162,357:165,344 +"black":241,304:229,298:219,306:216,319:230,325:241,320:251,310:251,299:252,310:257,340:256,362:250,376:236,379:224,372:221,361 +"black":271,227:271,240:274,255:275,271:277,291:278,308:279,318 +"black":299,322:309,320:328,305:318,300:303,313:333,335:361,322 +"black":219,392:222,409:224,426:226,437 +"black":273,405:261,398:217,395:204,394:193,393:179,394 +"black":249,426:261,424:271,424:268,413:252,418:247,431:263,439:285,441:297,441 +"black":295,424:311,427:322,434 +"black":324,420:312,441:302,455 +"black":354,373:349,388:350,400:351,414:353,427:354,444 +"black":380,414:366,405:355,401:343,400:332,398:322,397 +"black":66,492:"Tap to enable" diff --git a/scribble/scribble.desktop b/scribble/scribble.desktop new file mode 100644 index 0000000..94385cc --- /dev/null +++ b/scribble/scribble.desktop @@ -0,0 +1,12 @@ +[Desktop Entry] +Name=Scribble pad +Comment=Note pad for scibbles and note taking +Encoding=UTF-8 +Version=1.0 +Type=Application +Exec=scribble.py +Icon=scribble +Terminal=false +Categories=GTK;Application;PIM;Office +SingleInstance=true +StartupNotify=true diff --git a/scribble/scribble.png b/scribble/scribble.png new file mode 100644 index 0000000000000000000000000000000000000000..a5b9cbc338e840319b681a3f67209c581c6f891f GIT binary patch literal 10593 zcmcgy^;cA3*BwCVl5RwlP+B^b6ln>`K|qF(?vRp{mhNr^q+y5wk?u}O>1ODBAOC=F zt@np_eTy}Vi964I?z!jez0W@LRaIFI51R@b0)gPkzma|ifuMk=C=g6^@K4{N2=?!l z<2yM?NZBCG7I=YftRN>1x&QZ<)m#`0-odhaqvHsHkfr{6pb(Eww1YP>oaB{cFs3mn z(C8oWbVvVNhSEt!+eymS+S=5{2_ofSYUE`4@v*Cgllfygc_r2Ne)!}N$YY4S^lJ^b z*}XsRpEV|K?~kU^lSLFXIR^Zr-Fh>SxX;CWZL#z z#)eg+=GKl?^ssR{jBEVxl@nvHt~?O6&nB#x*m!9se&p;34-5?O^6}^F=+-56z;K?h zvC(wAN`L3FJ)RNE{8l|Rb+4#|QbrBl{0d&S>d?TII)JPERubtUr1kDgP-IVD&V`Y` zg_*F1wDf}`EG%CBamR~81{!<}c)=y&`~@~_LeWZ}MqVmt)T&PX-MjD+^IzsX&YtP+ zqBiq30{*cB^RtmLVz>Z#11r8M3qW#h*X?J~6wuW?^Bm9CmwMBy8}tOvb2guHmx=kJ@c+ zV{;kR_Rh@prI|*$xED-sJDfK0?HltVpW65`bse3yY&65+@##0#*6fdNuego>t|hT2 z*szC9?W}3bb2Gn3)iBql0G9;r`j;F+GCF0uHzA9)u{kW#=TQY0-H*}+$x`-Q z(4R5oqzwFllI4!PQM6*iKwO|E_C*xe_y*PH9W(p4g_Jc8JLARnY@YK_;+pL|Y=Yvc zE?N0&p)#Njjz^7N*~VHPe@ahQ+)iv~%&#fC(F6;H6ntW#;G()+Od#<`SyS{fhX;-1 zU6m@z`ae<{)1B*g$75}Mpq1?8WFR*+c+*JS=i+jg+wPoq-8gvCI^*rJ5#O(L<1EWuGq*=0=0OtH@#}E8-K)IF@%itmk|U4ixVX6C;o;VwM(vnbSQc}! zuGN*i26AJ~Zq08S39OyLQ(h3vl!9!Q(#(-vJG+ZRBiX{d3k}=!k&rZHW=%~^5Zlq| z={J&+l4d79nRDtE7Oc!F86U@s)TE`Qo$6U=SrZB@Xhd9aL3x6Mx@Je;|C*f|8g_0j zy>+V2Oct)(80L+Li7^(x_`uAc8o_PS=~Se)ZOEEeo|#+nJU>7G^KlQeVw&JLuvdKR zdd{%C8afmfDi4tti0bM%M!6^=Iyz-3FEn(2l+*!PXJ@`OQc`_!hddSPlX>Ce)9$>A zif?Eb_=NQILp`y~U!$T9;u!f2dsj|9ggj1I@bK`$DPMM;*E&JX#=JNb?CrT9K76QM zsx>+~O2PB-%|`Vz`X~svR8mq>X<1pK&+BoYnqP1($RjO2<`cZ>ICvZ zpTle}?F(nH?uJ$ir|GCOZE!B764JOv7Yh#$z)Vf#qSp+XTl2T9v+pamH&4A$Gm*&Hdt@vLoFB{QXDlAl4 z%Xe*9o7T-0mzH)|X{l+(s+ih07?_()(kIR0t29G1iCCXJ`4Wh;J~lk;o16bIs>^?* zD?s%823}_2Cbnl-zdXUKD9gC2uKqP7r1c|D`qWpinVn2A;Sp!NJw?hny*+e)NjGf@ zj=I#)-nWvHU+f#_(}dlzmBxmfpuF_kd-H)$;boWA)6cqLj0_BZvnf=@#wK1+uCdwH z)7s{;_0#G`r7ieimY@I687o@ddPjbWp~)~#YplnD-JR3ixoVp6k5fz4XGdqw_WBO0 zblGvK`APh9F7(ODyT>&SpI}N961$xI+}ll>NhHDx`6^^PuZ+>z zLr!Cy$*S%t!7a{#ugX=-^y}9zu2fAcerhEprBW0l-rB&haz}%df=ey4P)P>|o-|6m zEBNgo`~A#b;ic-i{gFMbrCj<<0wf0mck`q+&q6oY+fc68!NwqI@cUzlGP|)MlKu~> zv-~aZ$AmSO)71*949nAK6*8>H_h?YlJiWy^_}817%X3()+g8+BPs@2rlr8i2N?#lZ zdFIuohtki+c^BNWSmQwnqHFUqUGrwY<$jG6bQMWflqIH%N`*DjR?YY*`2DdF0I#B> z2}wG9n~Sj$$G(o3pXen6J_$u-iv0$7EsY%?5GcDV%w+pbVU>0%1ui-p?4`4Fjdtmz zvbcfsRzyj0#iPFDgL4nD>C}{;+HiN)!{i3Sy1BpsV-%UT5v#75(Um&Vv{D5>JzbHs zk$@TFg6uN13_&cT1lwrUMpGS$>Gy82|lIpH++tnLOTEfrLr=? ztDKPFaMJzght_w)$$2-vQfKzY5+W+9USAaMG(oku*475EeyFGpvQ>YL2z-vh-!kF}ghXiDtF5C| z5_+-iI&e3HBa|YFn1}&ro0}z|&+WNrIG&e27xRpR*;W!idgM0%lM*t}X(%i%=B~+U zN?V6D8dBAvYM1ioU`P|wjS%?4XLX;ycwsNBcQe|C8!Bn{1{2T-;KA2pyi`=%#l^)( z?vlk-XpdP01Co*`G&BLusv2m@I6ISaNa*_&mOTIV?VI#zl&q%au98w*V&a&U?Xd-2 z7Nd2KaRrgs2M8dwY?j{ST8)uf@-sO-Y;1g3t8daIWVlHq-R6&$jq5qo?-MTtCJuqw zD@9zb@1MKkJl*a+l;OQF(Cc{%m03i`X5)%u;5b(hF|Mz2*$C{7- zyOV}Q3VpudvZ>R14)*_`a!BGnZQ}dluQdnfSu;9er;s^Ygnv%Xlj09pkVoD8HnYEN zGw$&~$){g60IqOrhW`76LE4r3a#f+ucCpR#pn1wPvz!HcYpW`m%jk=h(MUmAS>%+V zkN@{Fc<4y3dY-UFEXbahCr}=&lfzStmkTv()AEhy1G=MIo)K0 zzoOvQM8m5`KfQ0!e=$Dk?(dEs_=-pT*3kL_oOGbYT`ua?O5ykEd9*z)RKE9*PP*SV;`mYah){(!ELnb)bFV_e<;Evg16;g731u`uLHg zc(qd=4`*ZwHLIm10~8Gv6;*yv;N~Zk=)}yS3>9$(LBThc+954u5SPaEfB-kxU>YkQ zo3tSgL`Ln6ynLHE?_2QoMAR%eJVne?oB{R{ulgZuWMSh! z3B7DEKX7pw-MCSrVvdlbwZdwQxw`*!25hRMWbFuhp5LWn5i@fslc8WeIuv$hwR3cQ z`F+s?$4mhSsFMd67z+PR6$FWhhzO??32O7lP}svBA>RO*?ZpLI@VWQebrE@Q=o~-( zgV2i|a<(@YJht_ek5AK53lp62e^&z1t`B@i%%#;@x14fz;NxY4UaSxxGBC05m(2(y zB%x5cu1MPUol_9#5pvWI-O<_osS@iKd$zZ?LqbBLLpNFFAdr$0z~HQ$2Xl_?O4rMe{J?W?@fEBB8YlX5^&+>eb?RI zbbog(dec4RzP(kbQ~yDHW@ZKmj9ivbD-?S_ka*rleTEL(8k9bFNJCXk;`2pU`YC-! zJann4jQZI|S5CcJo71i4*RNlHG&3V6r37ph2Z4aieYm^13xV)~Mu+Z%3!e)Mbd5HY__3LuOhIe}q9(59zktnRHL7`=6&joq8k%??R@QT_g z)0^Etf3K-o;c=?hbUgKby563VK;nk0J(wUCBKYF@^H7n4CjaVcozI7D$}R^B11j`a zl(PPQFPN29;Q0#O-QCd%oP&;Qr0=Y{`e72Rv7!<&?`P|UVn6K ztU{Y@bi2cyjJH)lyH zc=);b3e*c|Npoc*j6YjGpW-we-2dYV!t5>M=vYdhc zzwF>FDlOfsjy$`=ppQ0GeKm5n=T!hz@_QFOk}LO;Su7pg#-71J7+A2R96{vM{1Cg{M7l?b$_|egK32e}Q()aqWt*oqq*D*pD*o}XO zd{aynRzl5c)G5{4Z`iPD5w!E2vuTXj-L)cP*OlI`xA$g^>RVfxQW5Kzvn{tc7uq~3 zE(RI)h?u%>oK+ouGp=c9R8g&rjoP2W7khExCVqEe0(G;02B3gVr!wjKps`$v;a^Mq z`E%L>UkQ-Xj(r0H>iYVMMn(*Cb+#K7q%V?$oh|DU*AAB%|E`xYI?oH1)vtuQG@rNn zp^3tcA68UUfOHDhLm+9dRd+`QxhT4PI|T*+RbkY&^{+=Onb83}d>qNKg2+O}W6UzWBFUlpUrS2%_5)}~ z=I8qncc+}n>B1fU1mdkR9bt1~PJ#}9F#`hw0Tujo>KP3~Z*B3q4vC6F{537VKHD=H zn63G{wx+J8rmU#AE8rBvMMO*tI+4Bj2#~}Yn$KRoM2qTb&rmr#Tn?spVS&V;6C^+| zAK-(d^$4Nq|u zAB()$q28e3s6igg)`Wp%t#aBT${hSvug?J*90JgW0SRSDxR2ZzG8@kFOH%Zk*}Xd5 zk^A`ZW2SDlCHCK61L-e=;j_Y8%{M%p_ggBEyh!S<>a8(tE+27F&FOdP82B_F$JPhf z8ux{5>W_l(BbZ}jW9Pms8C!m`v=cTU_Zk?Z^?k2V4flik{+Z>=DDzc&QJSQ*#vgOPP{2YdOPdV z{QSVmN}lH`nX*%-_<^r9HAzcKh)77Dq6|Mm_xIZ=Hyj)?^w^{MW-b2x#oXeZvX_@wiRBY_4W7`P!#&%3@+o!$kA>*YWjnjYTY{gi z2T;sbNcRxuht&e5dBA+M;;7wyegA^{@grLtWD^%e?I{xzkNt`?p!DvZfwhaH)tfs4 zM4??D8JVFcdlhWpDGyJj)S|jZb^7+UWro)oCCCs;LPT>;&X@QXJ_^eT?DMV%&GUBc zcqkRBy~}n#T?TZYJb9vByDQuoLED$-h>HAfdg3Pfkxjq5Q;Ay0?U0aBR-a7O`bSAg zDBy(J{H>Of;^Jiu9ZEGdHHQboGi+oyBO@c9&nNXpoYQ~)0=00hk9{2s*(9Nct1a8q$ao?VCZyf8q z_nh^8Lw2%a-hlGIIAFz7I+2MJ22wpG%F1DBRQfbPJsxTH5nW=2C()A4WjHy4BiBFnew z=5x@~XP3@@?iTOQeR_I&OxODCOfyX)dvzKQ9N!R&BzVUOjT@kWgV zu>ShkQyq16;#2`^CeU;C_bVcls@DLvT2Ayq*bLyjOPx0xvxs{feyjO^c2{}5tCX?v zlVr+dfU>~OU8b|AMf9V5_7CknGicFMS8vAbA!hr7=SY&yT{ zD@@Q@GMb^B$&{4d5+3uIY^;Kd z*5orK^lkXyM&^O($tH#5%_Z|Uz^ zS5-t&TK~$&YBycz0<{U>g$58o(jNl@M#ypWa+TiDa_UVh%nN$xBM>?rX>XiKaF zsgmNf=DyJ{l3G$i5xz&CeI?9lQKOqI>dv#;8DTP4>ndb-*qXdN=)DiWrrU4X;Q*Hv zp2GsP+)0RxI8f}v3lXD$vnpat!r#)ZO||<#CdR9%#-}SxzD7js#SDo%oUn`)Dt$Vy zp^*2-czJx>Cvx&f>wEkqa)aCFPBiPZ1;=&Xo;gbFjN<;HOX8JZzCuDwe0)bVgFF#L zOh;K6U&%!TP)5ATk_#pQfteNgfI8sa$)@I@+>Va8_;>?5&1C^0yCn$Fp1D<-^d!JQ z&yfiWiMZz3#m2^-*4OTCYH$P&A0_}&LlJi?UMjRZ8&xlUNN-G9M*0(1N=e%v|?>yG~lNnJ6iy|(@R3ugt^lX@F$%l?z0`v0PaB;G73szQ)kCDg* zhta1%;!UqD*4Ztif(G$nYgv@b(V&QsTk`@AbmPqJBPQXdaUSwIFEpv+&Mnhu7a_zE9l2&?+6D2XUAk&avlA` zO%L@|NI5qEsxr8xu>0jDD7%+H%8r^1wd9?*=xO{8rr&?U!NIXq-kb>8G`6p>4+spb zv|SXRsT2ztfH4XQCGX9JU3?s(Q;tv1DJWQ%Xc<~_sPMk^u;QP4_Qyv>Q*(3_<~%F* z)YBhInrvZF)ooB0S(xFGvJ_2NaJlUF*;7s53bN zJT^F%kq7N3-Nd9G+AB4Z^}+N$1J~OC;4D|#7-U#2(@{WVWMp`6gRgPTQ#vDQ57e}^ zyD^APh#(s&FU|0g>jSx!m7@pbmmy~W>He;5Ah*AluedZKj+{aJd-DZVR-gY17K%5L z1k2Y3#!J;R*t-&|BC$4Omf37BMMS%3a&WcDC~8Hejcq}wEh$8#P0)hi$lajiDY>S-pT7EbC&v5{WaC!M~-f1jCzs{-H z>H(H7P9SiOO}9xX>0K(avK|*GrS~4(A7Y^6z5Uz!!pk$Xs>9f5E9MylcyUS7)7qg5 zp`gfK&3y_DQ*g6idkcz32mTR@K-A8syn*rC1K-VWBr-bCu7rM!aI!H z#{HOI;L(ex0cR#+Bf~2aCi^HRgO49_xZf_3_y52a!{n@tfoZA6u>w60UJ7+MHTEc1H0sV z`|;@W@L+=MuFhruZyG}V+}l0ot6WmjB0Laxvd%X4q9)=z{=Y5PanJ8d}W?#JYO9B z*^y5x<9)+j+m+dRc7wxmdJ4Xkjq$^IQxyVwyw!Zo?=5H9m||+ib9qbcyYmgPiCU#O zO;Ks(%5UEqU!7Pu)X^Bai55`{(tqit78N%;o&Vci@^+_f$J zmgezjT^`IgPbX=56x;JV7IDW1Ef2=nM`QwexV%Zet2gxWWQTrc*U?FS@bKYj3%+Gt zuA68QHV*gR;^JlI;&_1jo=B<5T@uAfNC=%eA1$^oXmN3Ac>*G+1prTe4o7haR1YTD z2$xOZG?dlRq2w&n?^$jS=BD_3ZHEB4tz&vitR` zF5qTgxWxs(34lN_fByR|z`0GP1oM+8D>*OMwY~?M_9e!qrLhH%(q>yE@!NK4>(0*p zlvs#>o8quw3#QkTv$dut1ig5H)9iE4!TN=65?Q9Yd#K}!Ma;xyy{!Sn_qX_Xd60AE zlyW5{y%7Qnt>7_iy7-8WZcxm1@fmG>?wsYG$OEw};G+lX!*iB;yIWrxiNYsBMHm?$ zzh61MfPhKnvLD92*;?vJ@9!zG+zLDGO@b%mPA8vlWHuUmE#<)hB z#0843)ef3r&4t_6i2*HSSlGVy1fEawgY&Tj!WWDq^^d*&1lEr$mkZKRNL=G0MTCkT zJ~Uh~z!(l9$X(lSJSvEcg>UbJx)E?;s~Uq<*^e>(ixmRBHCrkeBItY^H;-Fg_{r+G zDl`(_K!_AM_)t`aUWfA@##Hu+%uDDlGE}lLpb+gV2Q~!c2>|%%W-|id%&`IkT8$)M z&!->AefOQcIv{5AYir4XchXP!G9)xKhW!sQAK#-Bq#U>HG=zgZ<3(Ak7!0?}9eJpn zHoOX(_jHCp@`f^$2L#WzS43$tAMufW;21qh!8MWakTx0-)t5Du`_ENa|pVA70U zdXCcgIw_9W3=FWGmevsn#KjwVuInf^x{f*YvAe+vFA{hy=-AA0qXvq>&6?u(g!57gTk0L!mCm*j>Z_AYsq4{q z;a!p0Hq#;Qw;mAOuBg>y!AH30G!mCs`++!+;UHYBq>XtTK#VwvIQkrljV{^yvw`+g za!F^m)CSy386xCj9Axam@Ka@Be!U$j4v!v#%zy{47XOUBpUN7a%qm;-1uKI}%qrQ; z&KC4OCKhF7bz@eJ8{|NF>_J*b&Bsjc_!?z9lD71`FO`pYC~2g=s`3z@@(UgDm zN{bD1U+uIN|65+#A!0?5q$rrk-tN4L=LUr3RV;*1EV3+DL(^!mr$bdY6coWb^$5>9 z)RWC}#9EC_%}Q^tB(>do%SCqfyC_N2EV)BBE*g=^GeoC~MEh|kb&-sWTW5H|nn>Le zEjQ{xo;=DeizV3TY~R|it6fKJ;Ng)Zem^aec!D}~p^-v=#k$|$KZ!g7)3y^HDCfG* z9nMZ4=W2!k`B3r(ySbVgfJ{iNJkuTsTzQ^__T35dj~6~+j+YL)-Zv&Ef;xtrPaz)k z*NKG(B#q714nt|N{$```)i+DmqnCjXUfJeoXyt%kJ!!l*alduO?RZw|Q|8X?b;7MH ziJE~$Y$tjF@pu=bkrWNt{6^a3$Q<-i<+*So-1NovU^-+I%DI1XQk(jrex0*5J=xEu zrnan?7+f{=`5-AN?H#W3j+SDh_^*?@Yl`h{4w{wVz~!bb+3I@1>LiMJ7UiSqEPNAT+Oxr9CO&{c^ z-aa)d>Ny*Uy$msGKp47;xVmpEt&|v0pFoz*DoFz|^B`Q-Gm?jY&h7TcQ^<&=`dn?BH;wAgkJa1? z6z>RviO`d)iJwHHwW=EUi?HKO)!9gfg4;C +# +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# The GNU General Public License, version 2, is available at +# http://www.fsf.org/licensing/licenses/info/GPLv2.html +# Or you can write to the Free Software Foundation, Inc., 51 +# Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# Author: Neil Brown +# Email: + + +# TODO +# - index page +# list of pages +# Buttons: select, delete, new +# - bigger buttons - fewer +# Need: undo, redo, colour, text/line +# When text is selected, these change to: +# align, join, make-title + +import pygtk +import gtk +import os +import pango +import gobject +import time +import listselect + +########################################################### +# Writing recognistion code +import math + + +def LoadDict(dict): + # Upper case. + # Where they are like lowercase, we either double + # the last stroke (L, J, I) or draw backwards (S, Z, X) + # U V are a special case + + dict.add('A', "R(4)6,8") + dict.add('B', "R(4)6,4.R(7)1,6") + dict.add('B', "R(4)6,4.L(4)2,8.R(7)1,6") + dict.add('B', "S(6)7,1.R(4)6,4.R(7)0,6") + dict.add('C', "R(4)8,2") + dict.add('D', "R(4)6,6") + dict.add('E', "L(1)2,8.L(7)2,8") + # double the stem for F + dict.add('F', "L(4)2,6.S(3)7,1") + dict.add('F', "S(1)5,3.S(3)1,7.S(3)7,1") + + dict.add('G', "L(4)2,5.S(8)1,7") + dict.add('G', "L(4)2,5.R(8)6,8") + # FIXME I need better straight-curve alignment + dict.add('H', "S(3)1,7.R(7)6,8.S(5)7,1") + dict.add('H', "L(3)0,5.R(7)6,8.S(5)7,1") + # capital I is down/up + dict.add('I', "S(4)1,7.S(4)7,1") + + # Capital J has a left/right tail + dict.add('J', "R(4)1,6.S(7)3,5") + + dict.add('K', "L(4)0,2.R(4)6,6.L(4)2,8") + + # Capital L, like J, doubles the foot + dict.add('L', "L(4)0,8.S(7)4,3") + + dict.add('M', "R(3)6,5.R(5)3,8") + dict.add('M', "R(3)6,5.L(1)0,2.R(5)3,8") + + dict.add('N', "R(3)6,8.L(5)0,2") + + # Capital O is CW, but can be CCW in special dict + dict.add('O', "R(4)1,1", bot='0') + + dict.add('P', "R(4)6,3") + dict.add('Q', "R(4)7,7.S(8)0,8") + + dict.add('R', "R(4)6,4.S(8)0,8") + + # S is drawn bottom to top. + dict.add('S', "L(7)6,1.R(1)7,2") + + # Double the stem for capital T + dict.add('T', "R(4)0,8.S(5)7,1") + + # U is L to R, V is R to L for now + dict.add('U', "L(4)0,2") + dict.add('V', "R(4)2,0") + + dict.add('W', "R(5)2,3.L(7)8,6.R(3)5,0") + dict.add('W', "R(5)2,3.R(3)5,0") + + dict.add('X', "R(4)6,0") + + dict.add('Y',"L(1)0,2.R(5)4,6.S(5)6,2") + dict.add('Y',"L(1)0,2.S(5)2,7.S(5)7,2") + + dict.add('Z', "R(4)8,2.L(4)6,0") + + # Lower case + dict.add('a', "L(4)2,2.L(5)1,7") + dict.add('a', "L(4)2,2.L(5)0,8") + dict.add('a', "L(4)2,2.S(5)0,8") + dict.add('b', "S(3)1,7.R(7)6,3") + dict.add('c', "L(4)2,8", top='C') + dict.add('d', "L(4)5,2.S(5)1,7") + dict.add('d', "L(4)5,2.L(5)0,8") + dict.add('e', "S(4)3,5.L(4)5,8") + dict.add('e', "L(4)3,8") + dict.add('f', "L(4)2,6", top='F') + dict.add('f', "S(1)5,3.S(3)1,7", top='F') + dict.add('g', "L(1)2,2.R(4)1,6") + dict.add('h', "S(3)1,7.R(7)6,8") + dict.add('h', "L(3)0,5.R(7)6,8") + dict.add('i', "S(4)1,7", top='I', bot='1') + dict.add('j', "R(4)1,6", top='J') + dict.add('k', "L(3)0,5.L(7)2,8") + dict.add('k', "L(4)0,5.R(7)6,6.L(7)1,8") + dict.add('l', "L(4)0,8", top='L') + dict.add('l', "S(4)0,8", top='L') + dict.add('l', "S(3)1,7.S(7)3,5", top='L') + dict.add('m', "S(3)1,7.R(3)6,8.R(5)6,8") + dict.add('m', "L(3)0,2.R(3)6,8.R(5)6,8") + dict.add('n', "S(3)1,7.R(4)6,8") + dict.add('o', "L(4)1,1", top='O', bot='0') + dict.add('p', "S(3)1,7.R(4)6,3") + dict.add('q', "L(1)2,2.L(5)1,5") + dict.add('q', "L(1)2,2.S(5)1,7.R(8)6,2") + dict.add('q', "L(1)2,2.S(5)1,7.S(5)1,7") + # FIXME this double 1,7 is due to a gentle where the + # second looks like a line because it is narrow.?? + dict.add('r', "S(3)1,7.R(4)6,2") + dict.add('s', "L(1)2,7.R(7)1,6", top='S', bot='5') + dict.add('s', "L(5)1,8.R(7)2,3", top='S', bot='5') + dict.add('t', "R(4)0,8", top='T', bot='7') + dict.add('t', "S(1)3,5.S(5)1,7", top='T', bot='7') + dict.add('u', "L(4)0,2.S(5)1,7") + dict.add('v', "L(4)0,2.L(2)0,2") + dict.add('w', "L(3)0,2.L(5)0,2", top='W') + dict.add('w', "L(3)0,5.R(7)6,8.L(5)3,2", top='W') + dict.add('w', "L(3)0,5.L(5)3,2", top='W') + dict.add('x', "L(4)0,6", top='X') + dict.add('y', "L(1)0,2.R(5)4,6", top='Y') # if curved + dict.add('y', "L(1)0,2.S(5)2,7", top='Y') + dict.add('z', "R(4)0,6.L(4)2,8", top='Z', bot='2') + + # Digits + dict.add('0', "L(4)7,7") + dict.add('0', "R(4)7,7") + dict.add('1', "S(4)7,1") + dict.add('2', "R(4)0,6.S(7)3,5") + dict.add('2', "R(4)3,6.L(4)2,8") + dict.add('3', "R(1)0,6.R(7)1,6") + dict.add('4', "L(4)7,5") + dict.add('5', "L(1)2,6.R(7)0,3") + dict.add('5', "L(1)2,6.L(4)0,8.R(7)0,3") + dict.add('6', "L(4)2,3") + dict.add('7', "S(1)3,5.R(4)1,6") + dict.add('7', "R(4)0,6") + dict.add('7', "R(4)0,7") + dict.add('8', "L(4)2,8.R(4)4,2.L(3)6,1") + dict.add('8', "L(1)2,8.R(7)2,0.L(1)6,1") + dict.add('8', "L(0)2,6.R(7)0,1.L(2)6,0") + dict.add('8', "R(4)2,6.L(4)4,2.R(5)8,1") + dict.add('9', "L(1)2,2.S(5)1,7") + + dict.add(' ', "S(4)3,5") + dict.add('', "S(4)5,3") + dict.add('-', "S(4)3,5.S(4)5,3") + dict.add('_', "S(4)3,5.S(4)5,3.S(4)3,5") + dict.add("", "S(4)5,3.S(3)3,5") + dict.add("","S(4)3,5.S(5)5,3") + dict.add("", "S(4)2,6") + + +class DictSegment: + # Each segment has four elements: + # direction: Right Straight Left (R=cw, L=ccw) + # location: 0-8. + # start: 0-8 + # finish: 0-8 + # Segments match if the difference at each element + # is 0, 1, or 3 (RSL coded as 012) + # A difference of 1 required both to be same / 3 + # On a match, return number of 0s + # On non-match, return -1 + def __init__(self, str): + # D(L)S,R + # 0123456 + self.e = [0,0,0,0] + if len(str) != 7: + raise ValueError + if str[1] != '(' or str[3] != ')' or str[5] != ',': + raise ValueError + if str[0] == 'R': + self.e[0] = 0 + elif str[0] == 'L': + self.e[0] = 2 + elif str[0] == 'S': + self.e[0] = 1 + else: + raise ValueError + + self.e[1] = int(str[2]) + self.e[2] = int(str[4]) + self.e[3] = int(str[6]) + + def match(self, other): + cnt = 0 + for i in range(0,4): + diff = abs(self.e[i] - other.e[i]) + if diff == 0: + cnt += 1 + elif diff == 3: + pass + elif diff == 1 and (self.e[i]/3 == other.e[i]/3): + pass + else: + return -1 + return cnt + +class DictPattern: + # A Dict Pattern is a list of segments. + # A parsed pattern matches a dict pattern if + # the are the same nubmer of segments and they + # all match. The value of the match is the sum + # of the individual matches. + # A DictPattern is printers as segments joined by periods. + # + def __init__(self, str): + self.segs = map(DictSegment, str.split(".")) + def match(self,other): + if len(self.segs) != len(other.segs): + return -1 + cnt = 0 + for i in range(0,len(self.segs)): + m = self.segs[i].match(other.segs[i]) + if m < 0: + return m + cnt += m + return cnt + + +class Dictionary: + # The dictionary hold all the pattern for symbols and + # performs lookup + # Each pattern in the directionary can be associated + # with 3 symbols. One when drawing in middle of screen, + # one for top of screen, one for bottom. + # Often these will all be the same. + # This allows e.g. s and S to have the same pattern in different + # location on the touchscreen. + # A match requires a unique entry with a match that is better + # than any other entry. + # + def __init__(self): + self.dict = [] + def add(self, sym, pat, top = None, bot = None): + if top == None: top = sym + if bot == None: bot = sym + self.dict.append((DictPattern(pat), sym, top, bot)) + + def _match(self, p): + max = -1 + val = None + for (ptn, sym, top, bot) in self.dict: + cnt = ptn.match(p) + if cnt > max: + max = cnt + val = (sym, top, bot) + elif cnt == max: + val = None + return val + + def match(self, str, pos = "mid"): + p = DictPattern(str) + m = self._match(p) + if m == None: + return m + (mid, top, bot) = self._match(p) + if pos == "top": return top + if pos == "bot": return bot + return mid + + +class Point: + # This represents a point in the path and all the points leading + # up to it. It allows us to find the direction and curvature from + # one point to another + # We store x,y, and sum/cnt of points so far + def __init__(self,x,y) : + self.xsum = x + self.ysum = y + self.x = x + self.y = y + self.cnt = 1 + + def copy(self): + n = Point(0,0) + n.xsum = self.xsum + n.ysum = self.ysum + n.x = self.x + n.y = self.y + n.cnt = self.cnt + return n + + def add(self,x,y): + if self.x == x and self.y == y: + return + self.x = x + self.y = y + self.xsum += x + self.ysum += y + self.cnt += 1 + + def xlen(self,p): + return abs(self.x - p.x) + def ylen(self,p): + return abs(self.y - p.y) + def sqlen(self,p): + x = self.x - p.x + y = self.y - p.y + return x*x + y*y + + def xdir(self,p): + if self.x > p.x: + return 1 + if self.x < p.x: + return -1 + return 0 + def ydir(self,p): + if self.y > p.y: + return 1 + if self.y < p.y: + return -1 + return 0 + def curve(self,p): + if self.cnt == p.cnt: + return 0 + x1 = p.x ; y1 = p.y + (x2,y2) = self.meanpoint(p) + x3 = self.x; y3 = self.y + + curve = (y3-y1)*(x2-x1) - (y2-y1)*(x3-x1) + curve = curve * 100 / ((y3-y1)*(y3-y1) + + (x3-x1)*(x3-x1)) + if curve > 6: + return 1 + if curve < -6: + return -1 + return 0 + + def Vcurve(self,p): + if self.cnt == p.cnt: + return 0 + x1 = p.x ; y1 = p.y + (x2,y2) = self.meanpoint(p) + x3 = self.x; y3 = self.y + + curve = (y3-y1)*(x2-x1) - (y2-y1)*(x3-x1) + curve = curve * 100 / ((y3-y1)*(y3-y1) + + (x3-x1)*(x3-x1)) + return curve + + def meanpoint(self,p): + x = (self.xsum - p.xsum) / (self.cnt - p.cnt) + y = (self.ysum - p.ysum) / (self.cnt - p.cnt) + return (x,y) + + def is_sharp(self,A,C): + # Measure the cosine at self between A and C + # as A and C could be curve, we take the mean point on + # self.A and self.C as the points to find cosine between + (ax,ay) = self.meanpoint(A) + (cx,cy) = self.meanpoint(C) + a = ax-self.x; b=ay-self.y + c = cx-self.x; d=cy-self.y + x = a*c + b*d + y = a*d - b*c + h = math.sqrt(x*x+y*y) + if h > 0: + cs = x*1000/h + else: + cs = 0 + return (cs > 900) + +class BBox: + # a BBox records min/max x/y of some Points and + # can subsequently report row, column, pos of each point + # can also locate one bbox in another + + def __init__(self, p): + self.minx = p.x + self.maxx = p.x + self.miny = p.y + self.maxy = p.y + + def width(self): + return self.maxx - self.minx + def height(self): + return self.maxy - self.miny + + def add(self, p): + if p.x > self.maxx: + self.maxx = p.x + if p.x < self.minx: + self.minx = p.x + + if p.y > self.maxy: + self.maxy = p.y + if p.y < self.miny: + self.miny = p.y + def finish(self, div = 3): + # if aspect ratio is bad, we adjust max/min accordingly + # before setting [xy][12]. We don't change self.min/max + # as they are used to place stroke in bigger bbox. + # Normally divisions are at 1/3 and 2/3. They can be moved + # by setting div e.g. 2 = 1/2 and 1/2 + (minx,miny,maxx,maxy) = (self.minx,self.miny,self.maxx,self.maxy) + if (maxx - minx) * 3 < (maxy - miny) * 2: + # too narrow + mid = int((maxx + minx)/2) + halfwidth = int ((maxy - miny)/3) + minx = mid - halfwidth + maxx = mid + halfwidth + if (maxy - miny) * 3 < (maxx - minx) * 2: + # too wide + mid = int((maxy + miny)/2) + halfheight = int ((maxx - minx)/3) + miny = mid - halfheight + maxy = mid + halfheight + + div1 = div - 1 + self.x1 = int((div1*minx + maxx)/div) + self.x2 = int((minx + div1*maxx)/div) + self.y1 = int((div1*miny + maxy)/div) + self.y2 = int((miny + div1*maxy)/div) + + def row(self, p): + # 0, 1, 2 - top to bottom + if p.y <= self.y1: + return 0 + if p.y < self.y2: + return 1 + return 2 + def col(self, p): + if p.x <= self.x1: + return 0 + if p.x < self.x2: + return 1 + return 2 + def box(self, p): + # 0 to 9 + return self.row(p) * 3 + self.col(p) + + def relpos(self,b): + # b is a box within self. find location 0-8 + if b.maxx < self.x2 and b.minx < self.x1: + x = 0 + elif b.minx > self.x1 and b.maxx > self.x2: + x = 2 + else: + x = 1 + if b.maxy < self.y2 and b.miny < self.y1: + y = 0 + elif b.miny > self.y1 and b.maxy > self.y2: + y = 2 + else: + y = 1 + return y*3 + x + + +def different(*args): + cur = 0 + for i in args: + if cur != 0 and i != 0 and cur != i: + return True + if cur == 0: + cur = i + return False + +def maxcurve(*args): + for i in args: + if i != 0: + return i + return 0 + +class PPath: + # a PPath refines a list of x,y points into a list of Points + # The Points mark out segments which end at significant Points + # such as inflections and reversals. + + def __init__(self, x,y): + + self.start = Point(x,y) + self.mid = Point(x,y) + self.curr = Point(x,y) + self.list = [ self.start ] + + def add(self, x, y): + self.curr.add(x,y) + + if ( (abs(self.mid.xdir(self.start) - self.curr.xdir(self.mid)) == 2) or + (abs(self.mid.ydir(self.start) - self.curr.ydir(self.mid)) == 2) or + (abs(self.curr.Vcurve(self.start))+2 < abs(self.mid.Vcurve(self.start)))): + pass + else: + self.mid = self.curr.copy() + + if self.curr.xlen(self.mid) > 4 or self.curr.ylen(self.mid) > 4: + self.start = self.mid.copy() + self.list.append(self.start) + self.mid = self.curr.copy() + + def close(self): + self.list.append(self.curr) + + def get_sectlist(self): + if len(self.list) <= 2: + return [[0,self.list]] + l = [] + A = self.list[0] + B = self.list[1] + s = [A,B] + curcurve = B.curve(A) + for C in self.list[2:]: + cabc = C.curve(A) + cab = B.curve(A) + cbc = C.curve(B) + if B.is_sharp(A,C) and not different(cabc, cab, cbc, curcurve): + # B is too pointy, must break here + l.append([curcurve, s]) + s = [B, C] + curcurve = cbc + elif not different(cabc, cab, cbc, curcurve): + # all happy + s.append(C) + if curcurve == 0: + curcurve = maxcurve(cab, cbc, cabc) + elif not different(cabc, cab, cbc) : + # gentle inflection along AB + # was: AB goes in old and new section + # now: AB only in old section, but curcurve + # preseved. + l.append([curcurve,s]) + s = [A, B, C] + curcurve =maxcurve(cab, cbc, cabc) + else: + # Change of direction at B + l.append([curcurve,s]) + s = [B, C] + curcurve = cbc + + A = B + B = C + l.append([curcurve,s]) + + return l + + def remove_shorts(self, bbox): + # in self.list, if a point is close to the previous point, + # remove it. + if len(self.list) <= 2: + return + w = bbox.width()/10 + h = bbox.height()/10 + n = [self.list[0]] + leng = w*h*2*2 + for p in self.list[1:]: + l = p.sqlen(n[-1]) + if l > leng: + n.append(p) + self.list = n + + def text(self): + # OK, we have a list of points with curvature between. + # want to divide this into sections. + # for each 3 consectutive points ABC curve of ABC and AB and BC + # If all the same, they are all in a section. + # If not B starts a new section and the old ends on B or C... + BB = BBox(self.list[0]) + for p in self.list: + BB.add(p) + BB.finish() + self.bbox = BB + self.remove_shorts(BB) + sectlist = self.get_sectlist() + t = "" + for c, s in sectlist: + if c > 0: + dr = "R" # clockwise is to the Right + elif c < 0: + dr = "L" # counterclockwise to the Left + else: + dr = "S" # straight + bb = BBox(s[0]) + for p in s: + bb.add(p) + bb.finish() + # If all points are in some row or column, then + # line is S + rwdiff = False; cldiff = False + rw = bb.row(s[0]); cl=bb.col(s[0]) + for p in s: + if bb.row(p) != rw: rwdiff = True + if bb.col(p) != cl: cldiff = True + if not rwdiff or not cldiff: dr = "S" + + t1 = dr + t1 += "(%d)" % BB.relpos(bb) + t1 += "%d,%d" % (bb.box(s[0]), bb.box(s[-1])) + t += t1 + '.' + return t[:-1] + + + +def page_cmp(a,b): + if a.lower() < b.lower(): + return -1 + if a.lower() > b.lower(): + return 1 + if a < b: + return -1 + if a > b: + return 1 + return 0 + +def inc_name(a): + l = len(a) + while l > 0 and a[l-1] >= '0' and a[l-1] <= '9': + l -= 1 + # a[l:] is the last number + if l == len(a): + # there is no number + return a + ".1" + num = 0 + int(a[l:]) + return a[0:l] + ("%d" % (num+1)) + +class ScribblePad: + + def __init__(self): + window = gtk.Window(gtk.WINDOW_TOPLEVEL) + window.connect("destroy", self.close_application) + window.set_title("ScribblePad") + #window.set_size_request(480,640) + self.window = window + + vb = gtk.VBox() + vb.show() + self.draw_box = vb + + bar = gtk.HBox(True) + bar.set_size_request(-1, 60) + vb.pack_end(bar, expand=False) + bar.show() + + l = gtk.Label('Page Name') + vb.pack_start(l, expand=False) + l.show() + self.name = l + + page = gtk.DrawingArea() + page.set_size_request(480,540) + vb.add(page) + page.show() + ctx = page.get_pango_context() + fd = ctx.get_font_description() + fd.set_absolute_size(25*pango.SCALE) + page.modify_font(fd) + + l.modify_font(fd) + + dflt = gtk.widget_get_default_style() + fd = dflt.font_desc + fd.set_absolute_size(25*pango.SCALE) + + self.pixbuf = None + self.width = 0 + self.height = 0 + self.need_redraw = True + self.zoomx = None; self.zoomy = None + + # Now the widgets: + + done = gtk.Button("Done"); done.show() + colbtn = gtk.Button("colour"); colbtn.show() + undo = gtk.Button('Undo') ; undo.show() + redo = gtk.Button('Redo') ; redo.show() + + done.child.modify_font(fd) + colbtn.child.modify_font(fd) + undo.child.modify_font(fd) + redo.child.modify_font(fd) + + bar.add(done) + bar.add(colbtn) + bar.add(undo) + bar.add(redo) + + colbtn.connect("clicked", self.colour_change) + undo.connect("clicked", self.undo) + redo.connect("clicked", self.redo) + done.connect("clicked", self.done) + + self.col_align = colbtn + self.undo_join = undo + self.redo_rename = redo + + self.page = page + self.colbtn = colbtn + self.line = None + self.lines = [] + self.hist = [] # undo history + self.selecting = False + self.textcurs = 0 + self.textstr = None + self.textpos = None + + + page.connect("button_press_event", self.press) + page.connect("button_release_event", self.release) + page.connect("motion_notify_event", self.motion) + page.connect("expose-event", self.refresh) + page.connect("configure-event", self.reconfigure) + page.connect("key_press_event", self.type) + page.set_events(gtk.gdk.EXPOSURE_MASK + | gtk.gdk.STRUCTURE_MASK + | gtk.gdk.BUTTON_PRESS_MASK + | gtk.gdk.BUTTON_RELEASE_MASK + | gtk.gdk.KEY_PRESS_MASK + | gtk.gdk.KEY_RELEASE_MASK + | gtk.gdk.POINTER_MOTION_MASK + | gtk.gdk.POINTER_MOTION_HINT_MASK) + page.set_property('can-focus', True) + + + # Now create the index window + # A listselect of all the pages, with a row of buttons: + # open delete new undelete + ls = listselect.ListSelect(center = False) + self.listsel = ls + + ls.connect('selected', self.page_select) + ls.set_colour('page','blue') + ls.set_zoom(38) + ls.show() + + vb = gtk.VBox() + vb.show() + self.list_box = vb + + vb.add(ls) + bar = gtk.HBox(True); bar.show() + bar.set_size_request(-1, 60) + b= gtk.Button("Open"); b.child.modify_font(fd); b.show(); bar.add(b); b.connect('clicked', self.open_page) + b= gtk.Button("Del"); b.child.modify_font(fd); b.show(); bar.add(b); b.connect('clicked', self.del_page) + b= gtk.Button("New"); b.child.modify_font(fd); b.show(); bar.add(b); b.connect('clicked', self.new_page) + b= gtk.Button("Undelete"); b.child.modify_font(fd); b.show(); bar.add(b); b.connect('clicked', self.undelete_pages) + + vb.pack_end(bar, expand=False) + + window.add(self.draw_box) + + window.set_default_size(480,640) + + window.show() + + + if 'HOME' in os.environ: + home = os.environ['HOME'] + else: + home = "" + if home == "" or home == "/": + home = "/home/root" + self.page_dir = home + '/Pages' + self.page_dir = '/data/Pages' + self.load_pages() + + colourmap = page.get_colormap() + self.colourmap = {} + self.colnames = [ 'black', 'red', 'blue', 'green', 'purple', 'pink', 'yellow' ] + for col in self.colnames: + c = gtk.gdk.color_parse(col) + gc = page.window.new_gc() + gc.line_width = 2 + gc.set_foreground(colourmap.alloc_color(c)) + self.colourmap[col] = gc + self.reset_colour = False + + self.colour_textmode = self.colourmap['blue'] + + self.colourname = "black" + self.colour = self.colourmap['black'] + self.colbtn.child.set_text('black') + self.bg = page.get_style().bg_gc[gtk.STATE_NORMAL] + + self.dict = Dictionary() + LoadDict(self.dict) + self.textstr = None + + + ctx = page.get_pango_context() + fd = ctx.get_font_description() + met = ctx.get_metrics(fd) + self.lineheight = (met.get_ascent() + met.get_descent()) / pango.SCALE + self.lineascent = met.get_ascent() / pango.SCALE + + self.timeout = None + + window.remove(self.draw_box) + window.add(self.list_box) + + + def close_application(self, widget): + self.save_page() + gtk.main_quit() + + def load_pages(self): + try: + os.mkdir(self.page_dir) + except: + pass + self.names = os.listdir(self.page_dir) + if len(self.names) == 0: + self.names.append("1") + self.names.sort(page_cmp) + self.pages = {} + self.pagenum = 0 + self.load_page() + self.update_list() + + def update_list(self): + l = [] + for p in self.names: + l.append([p,'page']) + self.listsel.list = l + self.listsel.list_changed() + self.listsel.select(self.pagenum) + return + + def page_select(self, list, item): + if item == None: + return + self.pagenum = item + + def open_page(self, b): + self.load_page() + self.window.remove(self.list_box) + self.window.add(self.draw_box) + + def done(self, b): + self.flush_text() + self.save_page() + self.window.remove(self.draw_box) + self.window.add(self.list_box) + + def del_page(self, b): + pass + + def new_page(self, b): + newname = self.choose_unique(self.names[self.pagenum]) + self.names = self.names[0:self.pagenum+1] + [ newname ] + \ + self.names[self.pagenum+1:] + self.pagenum += 1; + self.update_list() + self.listsel.select(self.pagenum) + + return + + def undelete_pages(self, b): + pass + + + def type(self, c, ev): + if ev.keyval == 65288: + self.add_sym('') + elif ev.string == '\r': + self.add_sym('') + else: + self.add_sym(ev.string) + + def press(self, c, ev): + # Start a new line + if self.timeout: + gobject.source_remove(self.timeout) + self.timeout = None + self.taptime = time.time() + self.movetext = None + c.grab_focus() + self.movetimeout = None + self.selecting = False + if self.selection: + self.selection = None + self.need_redraw = True + self.name_buttons() + self.redraw() + + self.line = [ self.colourname, [int(ev.x), int(ev.y)] ] + return + def release(self, c, ev): + if self.movetimeout: + gobject.source_remove(self.movetimeout) + self.movetimeout = None + + if self.movetext: + self.movetext = None + self.line = None + self.need_redraw = True + self.redraw() + return + if self.line == None: + return + + if self.selecting: + self.selecting = False + self.line = None + return + if self.timeout == None: + self.timeout = gobject.timeout_add(20*1000, self.tick) + + if len(self.line) == 2: + # just set a cursor + need_redraw = ( self.textstr != None) + oldpos = None + if not self.textstr: + oldpos = self.textpos + self.flush_text() + if need_redraw: + self.redraw() + (lineno,index) = self.find_text(self.line[1]) + + if lineno == None: + # new text, + pos = self.align_text(self.line[1]) + if oldpos and abs(pos[0]-oldpos[0]) < 40 and \ + abs(pos[1] - oldpos[1]) < 40: + # turn of text mode + self.flush_text() + self.line = None + self.need_redraw = True + self.setlabel() + return + self.textpos = pos + self.textstr = "" + self.textcurs = 0 + # draw the cursor + self.draw_text(pos, self.colour, "", 0) + self.line = None + else: + # clicked inside an old text. + # shuffle it to the top, open it, edit. + ln = self.lines[lineno] + self.lines = self.lines[:lineno] + self.lines[lineno+1:] + self.textpos = ln[1] + self.textstr = ln[2] + if ln[0] in self.colourmap: + self.colourname = ln[0] + else: + self.colourname = "black" + self.colbtn.child.set_text(self.colourname) + self.colour = self.colourmap[self.colourname] + self.textcurs = index + 1 + self.need_redraw = True + self.redraw() + self.setlabel() + self.line = None + return + if self.textstr != None: + sym = self.getsym() + if sym: + self.add_sym(sym) + else: + self.redraw() + self.line = None + self.reset_colour = True + return + + self.lines.append(self.line) + self.line = None + self.reset_colour = True + self.need_redraw = True + return + def motion(self, c, ev): + if self.line: + if ev.is_hint: + x, y, state = ev.window.get_pointer() + else: + x = ev.x + y = ev.y + x = int(x) + y = int(y) + prev = self.line[-1] + if not self.movetext and abs(prev[0] - x) < 10 and abs(prev[1] - y) < 10: + return + if not self.movetext and len(self.line) == 2 and time.time() - self.taptime > 0.5: + self.flush_text() + (lineno, index) = self.find_text(prev) + if lineno != None: + self.movetext = self.lines[lineno] + self.moveoffset = [prev[0]-self.movetext[1][0], prev[1]-self.movetext[1][1]] + if self.movetext: + self.movetext[1] = [x-self.moveoffset[0],y-self.moveoffset[1]] + self.need_redraw = True + self.redraw() + return + + if self.movetimeout: + gobject.source_remove(self.movetimeout) + self.movetimeout = gobject.timeout_add(650, self.movetick) + + if self.textstr != None: + c.window.draw_line(self.colour_textmode, prev[0],prev[1],x,y) + else: + c.window.draw_line(self.colour, prev[0],prev[1],x,y) + self.line.append([x,y]) + return + + def movetick(self): + # longish pause while drawing + self.flush_text() + self.selecting = True + self.need_redraw = True + self.redraw() + + def tick(self): + # nothing for 20 seconds, flush the page + self.save_page() + gobject.source_remove(self.timeout) + self.timeout = None + + def find_text(self, pos): + x = pos[0]; y = pos[1] + self.lineascent + for i in range(0, len(self.lines)): + p = self.lines[i] + if type(p[2]) != str: + continue + if x >= p[1][0] and y >= p[1][1] and y < p[1][1] + self.lineheight: + # could be this line - check more precisely + layout = self.page.create_pango_layout(p[2]+' ') + (ink, log) = layout.get_pixel_extents() + (ex,ey,ew,eh) = log + if x < p[1][0] + ex or x > p[1][0] + ex + ew or \ + y < p[1][1] + ey or \ + y > p[1][1] + ey + self.lineheight : + continue + # OK, it is in this one. Find out where. + (index, gr) = layout.xy_to_index((x - p[1][0] - ex) * pango.SCALE, + (y - p[1][1] - ey - self.lineheight) * pango.SCALE) + if index >= len(p[2]): + index = len(p[2])-1 + return (i, index) + return (None, None) + + def align_text(self, pos): + # align pos to existing text. + # if pos is near one-line-past a previous text, move the exactly + # one-line-past + x = pos[0]; y = pos[1] + self.lineascent + for l in self.lines: + if type(l[2]) != str: + continue + if abs(x - l[1][0]) > self.lineheight: + continue + if abs(y - (l[1][1] + self.lineheight)) > self.lineheight: + continue + return [ l[1][0], l[1][1] + self.lineheight ] + return pos + + def flush_text(self): + if self.textstr == None: + self.textpos = None + return + self.setlabel() + if len(self.textstr) == 0: + self.textstr = None + self.textpos = None + return + l = [self.colourname, self.textpos, self.textstr] + self.lines.append(l) + self.need_redraw = True + self.textstr = None + self.textpos = None + + def draw_text(self, pos, colour, str, cursor = None, drawable = None, selected=False): + if drawable == None: + drawable = self.page.window + layout = self.page.create_pango_layout(str) + if self.zoomx != None: + xs = 2; xo = -self.zoomx + ys = 2; yo = -self.zoomy + else: + xs = 1 ; xo = 0 + ys = 1 ; yo = 0 + (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents() + if len(pos) == 2 or pos[2] != ew or pos[3] != eh: + while len(pos) > 2: + pos.pop() + pos.append(ew) + pos.append(eh) + if selected: + drawable.draw_rectangle(self.colourmap['yellow'], True, + ex+pos[0], ey+pos[1]-self.lineascent, ew, eh) + drawable.draw_layout(colour, pos[0]*xs+xo, + (pos[1] - self.lineascent)*ys + yo, + layout) + if cursor != None: + (strong,weak) = layout.get_cursor_pos(cursor) + (x,y,width,height) = strong + x = pos[0] + x / pango.SCALE + y = pos[1] + drawable.draw_line(self.colour_textmode, + x*xs+xo,y*ys+yo, (x-3)*xs+xo, (y+3)*ys+yo) + drawable.draw_line(self.colour_textmode, + x*xs+xo,y*ys+yo, (x+3)*xs+xo, (y+3)*ys+yo) + + def add_sym(self, sym): + if self.textstr == None: + return + if sym == "": + if self.textcurs > 0: + self.textstr = self.textstr[0:self.textcurs-1]+ \ + self.textstr[self.textcurs:] + self.textcurs -= 1 + elif sym == "": + if self.textcurs > 0: + self.textcurs -= 1 + elif sym == "": + if self.textcurs < len(self.textstr): + self.textcurs += 1 + elif sym == "": + tail = self.textstr[self.textcurs:] + self.textstr = self.textstr[:self.textcurs] + oldpos = self.textpos + self.flush_text() + self.textcurs = len(tail) + self.textstr = tail + self.textpos = [ oldpos[0], oldpos[1] + + self.lineheight ] + else: + self.textstr = self.textstr[0:self.textcurs] + sym + \ + self.textstr[self.textcurs:] + self.textcurs += 1 + self.redraw() + + + def getsym(self): + alloc = self.page.get_allocation() + pagebb = BBox(Point(0,0)) + pagebb.add(Point(alloc.width, alloc.height)) + pagebb.finish(div = 2) + + p = PPath(self.line[1][0], self.line[1][1]) + for pp in self.line[1:]: + p.add(pp[0], pp[1]) + p.close() + patn = p.text() + pos = pagebb.relpos(p.bbox) + tpos = "mid" + if pos < 3: + tpos = "top" + if pos >= 6: + tpos = "bot" + sym = self.dict.match(patn, tpos) + if sym == None: + print "Failed to match pattern:", patn + return sym + + def refresh(self, area, ev): + self.redraw() + + def setlabel(self): + if self.textstr == None: + self.name.set_label('Page: ' + self.names[self.pagenum] + ' (draw)') + else: + self.name.set_label('Page: ' + self.names[self.pagenum] + ' (text)') + + def name_buttons(self): + if self.selection: + self.col_align.child.set_text('Align') + self.undo_join.child.set_text('Join') + self.redo_rename.child.set_text('Rename') + else: + self.col_align.child.set_text(self.colourname) + self.undo_join.child.set_text('Undo') + self.redo_rename.child.set_text('Redo') + + def redraw(self): + self.setlabel() + + if self.need_redraw: + self.draw_buf() + self.page.window.draw_drawable(self.bg, self.pixbuf, 0, 0, 0, 0, + self.width, self.height) + + if self.textstr != None: + self.draw_text(self.textpos, self.colour, self.textstr, + self.textcurs) + + return + def draw_buf(self): + if self.pixbuf == None: + alloc = self.page.get_allocation() + self.pixbuf = gtk.gdk.Pixmap(self.page.window, alloc.width, alloc.height) + self.width = alloc.width + self.height = alloc.height + + self.pixbuf.draw_rectangle(self.bg, True, 0, 0, + self.width, self.height) + if self.zoomx != None: + xs = 2; xo = -self.zoomx + ys = 2; yo = -self.zoomy + else: + xs = 1 ; xo = 0 + ys = 1 ; yo = 0 + self.selection = [] + for l in self.lines: + if l[0] in self.colourmap: + col = self.colourmap[l[0]] + else: + col = self.colourmap['black'] + st = l[1] + if type(l[2]) == list: + for p in l[2:]: + self.pixbuf.draw_line(col, st[0]*xs + xo, st[1]*ys + yo, + p[0]*xs + xo,p[1]* ys + yo) + st = p + if type(l[2]) == str: + # if this text is 'near' the current line, make it red + selected = False + if self.selecting and self.line and len(st) == 4: + for p in self.line[1:]: + if p[0] > st[0] and \ + p[0] < st[0] + st[2] and \ + p[1] > st[1] - self.lineascent and \ + p[1] < st[1] - self.lineascent + st[3]: + selected = True + break + if selected: + self.selection.append(l) + self.draw_text(st, col, l[2], drawable = self.pixbuf, + selected = selected) + self.need_redraw = False + self.name_buttons() + + + def reconfigure(self, w, ev): + alloc = w.get_allocation() + if self.pixbuf == None: + return + if alloc.width != self.width or alloc.height != self.height: + self.pixbuf = None + self.need_redraw = True + + + def colour_change(self,t): + if self.selection: + # button is 'join' not 'colour' + return self.realign(t) + + if self.reset_colour and self.colourname != 'black': + next = 'black' + else: + next = 'black' + prev = '' + for c in self.colnames: + if self.colourname == prev: + next = c + prev = c + self.reset_colour = False + self.colourname = next + self.colour = self.colourmap[next] + t.child.set_text(next) + if self.textstr: + self.draw_text(self.textpos, self.colour, self.textstr, + self.textcurs) + + return + def text_change(self,t): + self.flush_text() + return + def undo(self,b): + if self.selection: + return self.join(b) + + if len(self.lines) == 0: + return + self.hist.append(self.lines.pop()) + self.need_redraw = True + self.redraw() + return + def redo(self,b): + if self.selection: + self.rename(self.selection[0][2]) + return + if len(self.hist) == 0: + return + self.lines.append(self.hist.pop()) + self.need_redraw = True + self.redraw() + return + def choose_unique(self, newname): + while newname in self.names: + new2 = inc_name(newname) + if new2 not in self.names: + newname = new2 + elif (newname + ".1") not in self.names: + newname = newname + ".1" + else: + newname = newname + ".0.1" + + return newname + def delete(self,b): + # hack + if self.selection: + return self.join(b) + self.flush_text() + if len(self.names) <= 1: + return + if len(self.lines) > 0: + return + self.save_page() + nm = self.names[self.pagenum] + if nm in self.pages: + del self.pages[nm] + self.names = self.names[0:self.pagenum] + self.names[self.pagenum+1:] + if self.pagenum >= len(self.names): + self.pagenum -= 1 + self.load_page() + self.need_redraw = True + self.redraw() + + return + + def cmplines(self, a,b): + pa = a[1] + pb = b[1] + if pa[1] != pb[1]: + return pa[1] - pb[1] + return pa[0] - pb[0] + + def realign(self, b): + self.selection.sort(self.cmplines) + x = self.selection[0][1][0] + y = self.selection[0][1][1] + for i in range(len(self.selection)): + self.selection[i][1][0] = x + self.selection[i][1][1] = y + y += self.lineheight + self.need_redraw = True + self.redraw() + + def join(self, b): + self.selection.sort(self.cmplines) + txt = "" + for i in range(len(self.selection)): + if txt: + txt = txt + ' ' + self.selection[i][2] + else: + txt = self.selection[i][2] + self.selection[i][2] = None + self.selection[0][2] = txt + i = 0; + while i < len(self.lines): + if len(self.lines[i]) > 2 and self.lines[i][2] == None: + self.lines = self.lines[:i] + self.lines[i+1:] + else: + i += 1 + self.need_redraw = True + self.redraw() + + + def rename(self, newname): + # Rename current page and rename the file + if self.names[self.pagenum] == newname: + return + self.save_page() + newname = self.choose_unique(newname) + oldpath = self.page_dir + "/" + self.names[self.pagenum] + newpath = self.page_dir + "/" + newname + try : + os.rename(oldpath, newpath) + self.names[self.pagenum] = newname + self.names.sort(page_cmp) + self.pagenum = self.names.index(newname) + self.setlabel() + except: + pass + self.update_list() + + def setname(self,b): + if self.textstr: + if len(self.textstr) > 0: + self.rename(self.textstr) + + def clear(self,b): + while len(self.lines) > 0: + self.hist.append(self.lines.pop()) + self.need_redraw = True + self.redraw() + return + + def parseline(self, l): + # string in "", or num,num. ':' separates words + words = l.strip().split(':') + line = [] + for w in words: + if w[0] == '"': + w = w[1:-1] + elif w.find(',') >= 0: + n = w.find(',') + x = int(w[:n]) + y = int(w[n+1:]) + w = [x,y] + line.append(w) + return line + + def load_page(self): + self.need_redraw = True + nm = self.names[self.pagenum] + if nm in self.pages: + self.lines = self.pages[nm] + return + self.lines = []; + try: + f = open(self.page_dir + "/" + self.names[self.pagenum], "r") + except: + f = None + if f: + l = f.readline() + while len(l) > 0: + self.lines.append(self.parseline(l)) + l = f.readline() + f.close() + return + + def save_page(self): + t = self.textstr; tc = self.textcurs + self.flush_text() + self.pages[self.names[self.pagenum]] = self.lines + tosave = self.lines + if t and len(t): + # restore the text + ln = self.lines[-1] + self.lines = self.lines[:-1] + self.textpos = ln[1] + self.textstr = ln[2] + self.textcurs = tc + + fn = self.page_dir + "/" + self.names[self.pagenum] + if len(tosave) == 0: + try: + os.unlink(fn) + except: + pass + return + f = open(fn, "w") + for l in tosave: + start = True + if not l: + continue + for w in l: + if not start: + f.write(":") + start = False + if isinstance(w, str): + f.write('"%s"' % w) + elif isinstance(w, list): + f.write("%d,%d" %( w[0],w[1])) + f.write("\n") + f.close() + +def main(): + gtk.main() + return 0 +if __name__ == "__main__": + ScribblePad() + main() diff --git a/scribble/scribble/Makefile b/scribble/scribble/Makefile new file mode 100644 index 0000000..fb0db4e --- /dev/null +++ b/scribble/scribble/Makefile @@ -0,0 +1,13 @@ + +oldinstall: + cp scribble.py /usr/bin + chmod a+rx /usr/bin/scribble.py + cp scribble.desktop /usr/share/applications + chmod a+r /usr/share/applications/scribble.desktop + cp scribble.png /usr/share/pixmaps/scribble.png + chmod a+r /usr/share/pixmaps/scribble.png + if [ -d $$HOME/Pages ] ; then : ; else cp -r Sample-Pages $$HOME/Pages; fi + +install: + chmod +x scribble.py + $(CP) scribble.py $(DEST)/usr/local/bin/scribble diff --git a/scribble/scribble/Sample-Pages/1 b/scribble/scribble/Sample-Pages/1 new file mode 100755 index 0000000..aea83a1 --- /dev/null +++ b/scribble/scribble/Sample-Pages/1 @@ -0,0 +1,19 @@ +"black":64,81:"Welcome to" +"black":72,159:62,162:45,170:34,178:36,192:48,201:63,207:79,214:92,226:97,245:95,263:83,278:65,287:51,289:50,278 +"black":146,200:136,204:122,212:120,241:131,246:144,249:159,243:169,235 +"black":177,206:188,221:193,231:186,211:186,199:199,187:209,183:219,182:234,181:238,191 +"black":255,186:261,196:270,213 +"black":263,111:270,142:276,170:280,186:284,199:286,209:292,224:292,213:293,203:294,192:299,178:317,187:317,201:307,210:292,202 +"black":321,103:321,119:327,156:330,176:332,193:333,207:337,194:345,175:357,171:365,182:360,197:333,194 +"black":371,96:371,108:374,124:376,138:380,154:382,171:387,195 +"black":416,182:421,171:433,161:429,151:417,155:407,168:405,183:410,193:422,196:454,183 +"red":433,211:423,212:405,214:393,215:380,218:367,221:355,225:344,230:332,233:319,237:307,241:295,245:283,250:258,258:235,264:224,267:214,270:201,273:189,276:177,279:153,285:142,289:129,293:117,296:106,300:96,303:79,310:63,316 +"black":92,384:91,396:92,417:91,429:89,417:88,400:90,385:94,371:102,360:113,357:123,359:131,371:129,383:123,394:112,402:100,403 +"black":133,403:139,414:137,401:148,390:162,390:173,395 +"black":174,411:189,407:200,404:208,394:198,388:186,391:178,403:182,418:195,426:208,426:221,422 +"black":253,398:240,394:227,396:232,406:243,410:225,421 +"black":288,405:264,404:275,413:286,418:276,428:259,431:249,431 +"red":359,349:370,359:381,370:391,377:404,391:399,401:389,407:379,411:366,418:356,425:346,431 +"red":94,506:"to continue" +"red":65,48:71,36:70,23:69,12:59,23 +"red":68,7:76,17:85,29 diff --git a/scribble/scribble/Sample-Pages/2 b/scribble/scribble/Sample-Pages/2 new file mode 100755 index 0000000..bc60b71 --- /dev/null +++ b/scribble/scribble/Sample-Pages/2 @@ -0,0 +1,23 @@ +"black":31,42:"Here is a page number" +"black":350,36:361,38:372,40:383,36:395,35:405,33:416,29:426,25:441,18:450,7:440,8:451,7:454,17:453,27 +"black":33,103:"Use" +"black":202,82:192,80:180,90:168,101:158,106:171,118:189,129:199,135 +"black":241,113:"To go back" +"black":92,161:106,168:116,174:128,183:140,193:134,204:124,214:113,225:103,234:93,242 +"black":176,198:"for next" +"black":58,259:54,274:58,291:65,303:79,308:91,303:101,264:101,254 +"black":112,276:117,289:120,300:124,289:138,284:149,295 +"black":192,283:182,282:172,285:170,296:180,298:190,290:192,279:191,265:186,246:183,234:186,251:189,262:190,273:192,284:194,294:197,304 +"black":217,291:226,280:236,278:247,287:245,299:226,305:213,294:215,284:225,280:241,280 +"black":58,349:67,359:71,371:70,360:71,350:77,338:90,335:102,337:112,342 +"black":123,352:134,355:146,355:156,354:163,343:151,338:135,340:129,356:141,371:167,375:184,372 +"black":224,336:222,346:208,350:201,364:208,376:218,372:225,357:225,337:222,321:224,342:227,353:230,363:235,373 +"black":261,361:262,349:274,343:285,348:288,359:272,374:258,369:258,359:264,349 +"black":65,406:68,395:69,409:71,425:73,436 +"black":86,412:76,414:58,416:46,416 +"black":110,436:"add page" +"black":96,499:84,499:73,500:63,500:53,502 +"black":121,510:"remove page" +"black":365,503:375,500:378,510:368,514:360,504:371,503 +"black":402,510:402,500:408,510:397,515:390,504:407,499 +"black":429,507:440,504:444,516:430,515:422,503:435,502 diff --git a/scribble/scribble/Sample-Pages/3 b/scribble/scribble/Sample-Pages/3 new file mode 100755 index 0000000..e3ddef8 --- /dev/null +++ b/scribble/scribble/Sample-Pages/3 @@ -0,0 +1,18 @@ +"black":117,45:122,57:108,51:96,48:82,53:70,62:57,74:47,86:41,100:40,117:41,128:48,146:56,156:66,165:78,171:110,177:133,169 +"black":158,119:"Clear page" +"black":195,159:"Before removal" +"black":83,235:84,224:80,250:79,270:78,289:78,319:79,330 +"black":165,229:149,223:128,220:105,218:60,213:46,213:36,213 +"black":120,308:130,303:143,298:156,306:154,320:139,329:129,326:127,308 +"black":186,297:175,305:168,315:178,318:189,315:199,304:201,321:203,332:169,367:162,357:165,344 +"black":241,304:229,298:219,306:216,319:230,325:241,320:251,310:251,299:252,310:257,340:256,362:250,376:236,379:224,372:221,361 +"black":271,227:271,240:274,255:275,271:277,291:278,308:279,318 +"black":299,322:309,320:328,305:318,300:303,313:333,335:361,322 +"black":219,392:222,409:224,426:226,437 +"black":273,405:261,398:217,395:204,394:193,393:179,394 +"black":249,426:261,424:271,424:268,413:252,418:247,431:263,439:285,441:297,441 +"black":295,424:311,427:322,434 +"black":324,420:312,441:302,455 +"black":354,373:349,388:350,400:351,414:353,427:354,444 +"black":380,414:366,405:355,401:343,400:332,398:322,397 +"black":66,492:"Tap to enable" diff --git a/scribble/scribble/scribble.desktop b/scribble/scribble/scribble.desktop new file mode 100644 index 0000000..94385cc --- /dev/null +++ b/scribble/scribble/scribble.desktop @@ -0,0 +1,12 @@ +[Desktop Entry] +Name=Scribble pad +Comment=Note pad for scibbles and note taking +Encoding=UTF-8 +Version=1.0 +Type=Application +Exec=scribble.py +Icon=scribble +Terminal=false +Categories=GTK;Application;PIM;Office +SingleInstance=true +StartupNotify=true diff --git a/scribble/scribble/scribble.png b/scribble/scribble/scribble.png new file mode 100644 index 0000000000000000000000000000000000000000..a5b9cbc338e840319b681a3f67209c581c6f891f GIT binary patch literal 10593 zcmcgy^;cA3*BwCVl5RwlP+B^b6ln>`K|qF(?vRp{mhNr^q+y5wk?u}O>1ODBAOC=F zt@np_eTy}Vi964I?z!jez0W@LRaIFI51R@b0)gPkzma|ifuMk=C=g6^@K4{N2=?!l z<2yM?NZBCG7I=YftRN>1x&QZ<)m#`0-odhaqvHsHkfr{6pb(Eww1YP>oaB{cFs3mn z(C8oWbVvVNhSEt!+eymS+S=5{2_ofSYUE`4@v*Cgllfygc_r2Ne)!}N$YY4S^lJ^b z*}XsRpEV|K?~kU^lSLFXIR^Zr-Fh>SxX;CWZL#z z#)eg+=GKl?^ssR{jBEVxl@nvHt~?O6&nB#x*m!9se&p;34-5?O^6}^F=+-56z;K?h zvC(wAN`L3FJ)RNE{8l|Rb+4#|QbrBl{0d&S>d?TII)JPERubtUr1kDgP-IVD&V`Y` zg_*F1wDf}`EG%CBamR~81{!<}c)=y&`~@~_LeWZ}MqVmt)T&PX-MjD+^IzsX&YtP+ zqBiq30{*cB^RtmLVz>Z#11r8M3qW#h*X?J~6wuW?^Bm9CmwMBy8}tOvb2guHmx=kJ@c+ zV{;kR_Rh@prI|*$xED-sJDfK0?HltVpW65`bse3yY&65+@##0#*6fdNuego>t|hT2 z*szC9?W}3bb2Gn3)iBql0G9;r`j;F+GCF0uHzA9)u{kW#=TQY0-H*}+$x`-Q z(4R5oqzwFllI4!PQM6*iKwO|E_C*xe_y*PH9W(p4g_Jc8JLARnY@YK_;+pL|Y=Yvc zE?N0&p)#Njjz^7N*~VHPe@ahQ+)iv~%&#fC(F6;H6ntW#;G()+Od#<`SyS{fhX;-1 zU6m@z`ae<{)1B*g$75}Mpq1?8WFR*+c+*JS=i+jg+wPoq-8gvCI^*rJ5#O(L<1EWuGq*=0=0OtH@#}E8-K)IF@%itmk|U4ixVX6C;o;VwM(vnbSQc}! zuGN*i26AJ~Zq08S39OyLQ(h3vl!9!Q(#(-vJG+ZRBiX{d3k}=!k&rZHW=%~^5Zlq| z={J&+l4d79nRDtE7Oc!F86U@s)TE`Qo$6U=SrZB@Xhd9aL3x6Mx@Je;|C*f|8g_0j zy>+V2Oct)(80L+Li7^(x_`uAc8o_PS=~Se)ZOEEeo|#+nJU>7G^KlQeVw&JLuvdKR zdd{%C8afmfDi4tti0bM%M!6^=Iyz-3FEn(2l+*!PXJ@`OQc`_!hddSPlX>Ce)9$>A zif?Eb_=NQILp`y~U!$T9;u!f2dsj|9ggj1I@bK`$DPMM;*E&JX#=JNb?CrT9K76QM zsx>+~O2PB-%|`Vz`X~svR8mq>X<1pK&+BoYnqP1($RjO2<`cZ>ICvZ zpTle}?F(nH?uJ$ir|GCOZE!B764JOv7Yh#$z)Vf#qSp+XTl2T9v+pamH&4A$Gm*&Hdt@vLoFB{QXDlAl4 z%Xe*9o7T-0mzH)|X{l+(s+ih07?_()(kIR0t29G1iCCXJ`4Wh;J~lk;o16bIs>^?* zD?s%823}_2Cbnl-zdXUKD9gC2uKqP7r1c|D`qWpinVn2A;Sp!NJw?hny*+e)NjGf@ zj=I#)-nWvHU+f#_(}dlzmBxmfpuF_kd-H)$;boWA)6cqLj0_BZvnf=@#wK1+uCdwH z)7s{;_0#G`r7ieimY@I687o@ddPjbWp~)~#YplnD-JR3ixoVp6k5fz4XGdqw_WBO0 zblGvK`APh9F7(ODyT>&SpI}N961$xI+}ll>NhHDx`6^^PuZ+>z zLr!Cy$*S%t!7a{#ugX=-^y}9zu2fAcerhEprBW0l-rB&haz}%df=ey4P)P>|o-|6m zEBNgo`~A#b;ic-i{gFMbrCj<<0wf0mck`q+&q6oY+fc68!NwqI@cUzlGP|)MlKu~> zv-~aZ$AmSO)71*949nAK6*8>H_h?YlJiWy^_}817%X3()+g8+BPs@2rlr8i2N?#lZ zdFIuohtki+c^BNWSmQwnqHFUqUGrwY<$jG6bQMWflqIH%N`*DjR?YY*`2DdF0I#B> z2}wG9n~Sj$$G(o3pXen6J_$u-iv0$7EsY%?5GcDV%w+pbVU>0%1ui-p?4`4Fjdtmz zvbcfsRzyj0#iPFDgL4nD>C}{;+HiN)!{i3Sy1BpsV-%UT5v#75(Um&Vv{D5>JzbHs zk$@TFg6uN13_&cT1lwrUMpGS$>Gy82|lIpH++tnLOTEfrLr=? ztDKPFaMJzght_w)$$2-vQfKzY5+W+9USAaMG(oku*475EeyFGpvQ>YL2z-vh-!kF}ghXiDtF5C| z5_+-iI&e3HBa|YFn1}&ro0}z|&+WNrIG&e27xRpR*;W!idgM0%lM*t}X(%i%=B~+U zN?V6D8dBAvYM1ioU`P|wjS%?4XLX;ycwsNBcQe|C8!Bn{1{2T-;KA2pyi`=%#l^)( z?vlk-XpdP01Co*`G&BLusv2m@I6ISaNa*_&mOTIV?VI#zl&q%au98w*V&a&U?Xd-2 z7Nd2KaRrgs2M8dwY?j{ST8)uf@-sO-Y;1g3t8daIWVlHq-R6&$jq5qo?-MTtCJuqw zD@9zb@1MKkJl*a+l;OQF(Cc{%m03i`X5)%u;5b(hF|Mz2*$C{7- zyOV}Q3VpudvZ>R14)*_`a!BGnZQ}dluQdnfSu;9er;s^Ygnv%Xlj09pkVoD8HnYEN zGw$&~$){g60IqOrhW`76LE4r3a#f+ucCpR#pn1wPvz!HcYpW`m%jk=h(MUmAS>%+V zkN@{Fc<4y3dY-UFEXbahCr}=&lfzStmkTv()AEhy1G=MIo)K0 zzoOvQM8m5`KfQ0!e=$Dk?(dEs_=-pT*3kL_oOGbYT`ua?O5ykEd9*z)RKE9*PP*SV;`mYah){(!ELnb)bFV_e<;Evg16;g731u`uLHg zc(qd=4`*ZwHLIm10~8Gv6;*yv;N~Zk=)}yS3>9$(LBThc+954u5SPaEfB-kxU>YkQ zo3tSgL`Ln6ynLHE?_2QoMAR%eJVne?oB{R{ulgZuWMSh! z3B7DEKX7pw-MCSrVvdlbwZdwQxw`*!25hRMWbFuhp5LWn5i@fslc8WeIuv$hwR3cQ z`F+s?$4mhSsFMd67z+PR6$FWhhzO??32O7lP}svBA>RO*?ZpLI@VWQebrE@Q=o~-( zgV2i|a<(@YJht_ek5AK53lp62e^&z1t`B@i%%#;@x14fz;NxY4UaSxxGBC05m(2(y zB%x5cu1MPUol_9#5pvWI-O<_osS@iKd$zZ?LqbBLLpNFFAdr$0z~HQ$2Xl_?O4rMe{J?W?@fEBB8YlX5^&+>eb?RI zbbog(dec4RzP(kbQ~yDHW@ZKmj9ivbD-?S_ka*rleTEL(8k9bFNJCXk;`2pU`YC-! zJann4jQZI|S5CcJo71i4*RNlHG&3V6r37ph2Z4aieYm^13xV)~Mu+Z%3!e)Mbd5HY__3LuOhIe}q9(59zktnRHL7`=6&joq8k%??R@QT_g z)0^Etf3K-o;c=?hbUgKby563VK;nk0J(wUCBKYF@^H7n4CjaVcozI7D$}R^B11j`a zl(PPQFPN29;Q0#O-QCd%oP&;Qr0=Y{`e72Rv7!<&?`P|UVn6K ztU{Y@bi2cyjJH)lyH zc=);b3e*c|Npoc*j6YjGpW-we-2dYV!t5>M=vYdhc zzwF>FDlOfsjy$`=ppQ0GeKm5n=T!hz@_QFOk}LO;Su7pg#-71J7+A2R96{vM{1Cg{M7l?b$_|egK32e}Q()aqWt*oqq*D*pD*o}XO zd{aynRzl5c)G5{4Z`iPD5w!E2vuTXj-L)cP*OlI`xA$g^>RVfxQW5Kzvn{tc7uq~3 zE(RI)h?u%>oK+ouGp=c9R8g&rjoP2W7khExCVqEe0(G;02B3gVr!wjKps`$v;a^Mq z`E%L>UkQ-Xj(r0H>iYVMMn(*Cb+#K7q%V?$oh|DU*AAB%|E`xYI?oH1)vtuQG@rNn zp^3tcA68UUfOHDhLm+9dRd+`QxhT4PI|T*+RbkY&^{+=Onb83}d>qNKg2+O}W6UzWBFUlpUrS2%_5)}~ z=I8qncc+}n>B1fU1mdkR9bt1~PJ#}9F#`hw0Tujo>KP3~Z*B3q4vC6F{537VKHD=H zn63G{wx+J8rmU#AE8rBvMMO*tI+4Bj2#~}Yn$KRoM2qTb&rmr#Tn?spVS&V;6C^+| zAK-(d^$4Nq|u zAB()$q28e3s6igg)`Wp%t#aBT${hSvug?J*90JgW0SRSDxR2ZzG8@kFOH%Zk*}Xd5 zk^A`ZW2SDlCHCK61L-e=;j_Y8%{M%p_ggBEyh!S<>a8(tE+27F&FOdP82B_F$JPhf z8ux{5>W_l(BbZ}jW9Pms8C!m`v=cTU_Zk?Z^?k2V4flik{+Z>=DDzc&QJSQ*#vgOPP{2YdOPdV z{QSVmN}lH`nX*%-_<^r9HAzcKh)77Dq6|Mm_xIZ=Hyj)?^w^{MW-b2x#oXeZvX_@wiRBY_4W7`P!#&%3@+o!$kA>*YWjnjYTY{gi z2T;sbNcRxuht&e5dBA+M;;7wyegA^{@grLtWD^%e?I{xzkNt`?p!DvZfwhaH)tfs4 zM4??D8JVFcdlhWpDGyJj)S|jZb^7+UWro)oCCCs;LPT>;&X@QXJ_^eT?DMV%&GUBc zcqkRBy~}n#T?TZYJb9vByDQuoLED$-h>HAfdg3Pfkxjq5Q;Ay0?U0aBR-a7O`bSAg zDBy(J{H>Of;^Jiu9ZEGdHHQboGi+oyBO@c9&nNXpoYQ~)0=00hk9{2s*(9Nct1a8q$ao?VCZyf8q z_nh^8Lw2%a-hlGIIAFz7I+2MJ22wpG%F1DBRQfbPJsxTH5nW=2C()A4WjHy4BiBFnew z=5x@~XP3@@?iTOQeR_I&OxODCOfyX)dvzKQ9N!R&BzVUOjT@kWgV zu>ShkQyq16;#2`^CeU;C_bVcls@DLvT2Ayq*bLyjOPx0xvxs{feyjO^c2{}5tCX?v zlVr+dfU>~OU8b|AMf9V5_7CknGicFMS8vAbA!hr7=SY&yT{ zD@@Q@GMb^B$&{4d5+3uIY^;Kd z*5orK^lkXyM&^O($tH#5%_Z|Uz^ zS5-t&TK~$&YBycz0<{U>g$58o(jNl@M#ypWa+TiDa_UVh%nN$xBM>?rX>XiKaF zsgmNf=DyJ{l3G$i5xz&CeI?9lQKOqI>dv#;8DTP4>ndb-*qXdN=)DiWrrU4X;Q*Hv zp2GsP+)0RxI8f}v3lXD$vnpat!r#)ZO||<#CdR9%#-}SxzD7js#SDo%oUn`)Dt$Vy zp^*2-czJx>Cvx&f>wEkqa)aCFPBiPZ1;=&Xo;gbFjN<;HOX8JZzCuDwe0)bVgFF#L zOh;K6U&%!TP)5ATk_#pQfteNgfI8sa$)@I@+>Va8_;>?5&1C^0yCn$Fp1D<-^d!JQ z&yfiWiMZz3#m2^-*4OTCYH$P&A0_}&LlJi?UMjRZ8&xlUNN-G9M*0(1N=e%v|?>yG~lNnJ6iy|(@R3ugt^lX@F$%l?z0`v0PaB;G73szQ)kCDg* zhta1%;!UqD*4Ztif(G$nYgv@b(V&QsTk`@AbmPqJBPQXdaUSwIFEpv+&Mnhu7a_zE9l2&?+6D2XUAk&avlA` zO%L@|NI5qEsxr8xu>0jDD7%+H%8r^1wd9?*=xO{8rr&?U!NIXq-kb>8G`6p>4+spb zv|SXRsT2ztfH4XQCGX9JU3?s(Q;tv1DJWQ%Xc<~_sPMk^u;QP4_Qyv>Q*(3_<~%F* z)YBhInrvZF)ooB0S(xFGvJ_2NaJlUF*;7s53bN zJT^F%kq7N3-Nd9G+AB4Z^}+N$1J~OC;4D|#7-U#2(@{WVWMp`6gRgPTQ#vDQ57e}^ zyD^APh#(s&FU|0g>jSx!m7@pbmmy~W>He;5Ah*AluedZKj+{aJd-DZVR-gY17K%5L z1k2Y3#!J;R*t-&|BC$4Omf37BMMS%3a&WcDC~8Hejcq}wEh$8#P0)hi$lajiDY>S-pT7EbC&v5{WaC!M~-f1jCzs{-H z>H(H7P9SiOO}9xX>0K(avK|*GrS~4(A7Y^6z5Uz!!pk$Xs>9f5E9MylcyUS7)7qg5 zp`gfK&3y_DQ*g6idkcz32mTR@K-A8syn*rC1K-VWBr-bCu7rM!aI!H z#{HOI;L(ex0cR#+Bf~2aCi^HRgO49_xZf_3_y52a!{n@tfoZA6u>w60UJ7+MHTEc1H0sV z`|;@W@L+=MuFhruZyG}V+}l0ot6WmjB0Laxvd%X4q9)=z{=Y5PanJ8d}W?#JYO9B z*^y5x<9)+j+m+dRc7wxmdJ4Xkjq$^IQxyVwyw!Zo?=5H9m||+ib9qbcyYmgPiCU#O zO;Ks(%5UEqU!7Pu)X^Bai55`{(tqit78N%;o&Vci@^+_f$J zmgezjT^`IgPbX=56x;JV7IDW1Ef2=nM`QwexV%Zet2gxWWQTrc*U?FS@bKYj3%+Gt zuA68QHV*gR;^JlI;&_1jo=B<5T@uAfNC=%eA1$^oXmN3Ac>*G+1prTe4o7haR1YTD z2$xOZG?dlRq2w&n?^$jS=BD_3ZHEB4tz&vitR` zF5qTgxWxs(34lN_fByR|z`0GP1oM+8D>*OMwY~?M_9e!qrLhH%(q>yE@!NK4>(0*p zlvs#>o8quw3#QkTv$dut1ig5H)9iE4!TN=65?Q9Yd#K}!Ma;xyy{!Sn_qX_Xd60AE zlyW5{y%7Qnt>7_iy7-8WZcxm1@fmG>?wsYG$OEw};G+lX!*iB;yIWrxiNYsBMHm?$ zzh61MfPhKnvLD92*;?vJ@9!zG+zLDGO@b%mPA8vlWHuUmE#<)hB z#0843)ef3r&4t_6i2*HSSlGVy1fEawgY&Tj!WWDq^^d*&1lEr$mkZKRNL=G0MTCkT zJ~Uh~z!(l9$X(lSJSvEcg>UbJx)E?;s~Uq<*^e>(ixmRBHCrkeBItY^H;-Fg_{r+G zDl`(_K!_AM_)t`aUWfA@##Hu+%uDDlGE}lLpb+gV2Q~!c2>|%%W-|id%&`IkT8$)M z&!->AefOQcIv{5AYir4XchXP!G9)xKhW!sQAK#-Bq#U>HG=zgZ<3(Ak7!0?}9eJpn zHoOX(_jHCp@`f^$2L#WzS43$tAMufW;21qh!8MWakTx0-)t5Du`_ENa|pVA70U zdXCcgIw_9W3=FWGmevsn#KjwVuInf^x{f*YvAe+vFA{hy=-AA0qXvq>&6?u(g!57gTk0L!mCm*j>Z_AYsq4{q z;a!p0Hq#;Qw;mAOuBg>y!AH30G!mCs`++!+;UHYBq>XtTK#VwvIQkrljV{^yvw`+g za!F^m)CSy386xCj9Axam@Ka@Be!U$j4v!v#%zy{47XOUBpUN7a%qm;-1uKI}%qrQ; z&KC4OCKhF7bz@eJ8{|NF>_J*b&Bsjc_!?z9lD71`FO`pYC~2g=s`3z@@(UgDm zN{bD1U+uIN|65+#A!0?5q$rrk-tN4L=LUr3RV;*1EV3+DL(^!mr$bdY6coWb^$5>9 z)RWC}#9EC_%}Q^tB(>do%SCqfyC_N2EV)BBE*g=^GeoC~MEh|kb&-sWTW5H|nn>Le zEjQ{xo;=DeizV3TY~R|it6fKJ;Ng)Zem^aec!D}~p^-v=#k$|$KZ!g7)3y^HDCfG* z9nMZ4=W2!k`B3r(ySbVgfJ{iNJkuTsTzQ^__T35dj~6~+j+YL)-Zv&Ef;xtrPaz)k z*NKG(B#q714nt|N{$```)i+DmqnCjXUfJeoXyt%kJ!!l*alduO?RZw|Q|8X?b;7MH ziJE~$Y$tjF@pu=bkrWNt{6^a3$Q<-i<+*So-1NovU^-+I%DI1XQk(jrex0*5J=xEu zrnan?7+f{=`5-AN?H#W3j+SDh_^*?@Yl`h{4w{wVz~!bb+3I@1>LiMJ7UiSqEPNAT+Oxr9CO&{c^ z-aa)d>Ny*Uy$msGKp47;xVmpEt&|v0pFoz*DoFz|^B`Q-Gm?jY&h7TcQ^<&=`dn?BH;wAgkJa1? z6z>RviO`d)iJwHHwW=EUi?HKO)!9gfg4;C +# +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# The GNU General Public License, version 2, is available at +# http://www.fsf.org/licensing/licenses/info/GPLv2.html +# Or you can write to the Free Software Foundation, Inc., 51 +# Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +# Author: Neil Brown +# Email: + + +# TODO +# - index page +# list of pages +# Buttons: select, delete, new +# - bigger buttons - fewer +# Need: undo, redo, colour, text/line +# When text is selected, these change to: +# align, join, make-title + +import pygtk +import gtk +import os +import pango +import gobject +import time +import listselect + +########################################################### +# Writing recognistion code +import math + + +def LoadDict(dict): + # Upper case. + # Where they are like lowercase, we either double + # the last stroke (L, J, I) or draw backwards (S, Z, X) + # U V are a special case + + dict.add('A', "R(4)6,8") + dict.add('B', "R(4)6,4.R(7)1,6") + dict.add('B', "R(4)6,4.L(4)2,8.R(7)1,6") + dict.add('B', "S(6)7,1.R(4)6,4.R(7)0,6") + dict.add('C', "R(4)8,2") + dict.add('D', "R(4)6,6") + dict.add('E', "L(1)2,8.L(7)2,8") + # double the stem for F + dict.add('F', "L(4)2,6.S(3)7,1") + dict.add('F', "S(1)5,3.S(3)1,7.S(3)7,1") + + dict.add('G', "L(4)2,5.S(8)1,7") + dict.add('G', "L(4)2,5.R(8)6,8") + # FIXME I need better straight-curve alignment + dict.add('H', "S(3)1,7.R(7)6,8.S(5)7,1") + dict.add('H', "L(3)0,5.R(7)6,8.S(5)7,1") + # capital I is down/up + dict.add('I', "S(4)1,7.S(4)7,1") + + # Capital J has a left/right tail + dict.add('J', "R(4)1,6.S(7)3,5") + + dict.add('K', "L(4)0,2.R(4)6,6.L(4)2,8") + + # Capital L, like J, doubles the foot + dict.add('L', "L(4)0,8.S(7)4,3") + + dict.add('M', "R(3)6,5.R(5)3,8") + dict.add('M', "R(3)6,5.L(1)0,2.R(5)3,8") + + dict.add('N', "R(3)6,8.L(5)0,2") + + # Capital O is CW, but can be CCW in special dict + dict.add('O', "R(4)1,1", bot='0') + + dict.add('P', "R(4)6,3") + dict.add('Q', "R(4)7,7.S(8)0,8") + + dict.add('R', "R(4)6,4.S(8)0,8") + + # S is drawn bottom to top. + dict.add('S', "L(7)6,1.R(1)7,2") + + # Double the stem for capital T + dict.add('T', "R(4)0,8.S(5)7,1") + + # U is L to R, V is R to L for now + dict.add('U', "L(4)0,2") + dict.add('V', "R(4)2,0") + + dict.add('W', "R(5)2,3.L(7)8,6.R(3)5,0") + dict.add('W', "R(5)2,3.R(3)5,0") + + dict.add('X', "R(4)6,0") + + dict.add('Y',"L(1)0,2.R(5)4,6.S(5)6,2") + dict.add('Y',"L(1)0,2.S(5)2,7.S(5)7,2") + + dict.add('Z', "R(4)8,2.L(4)6,0") + + # Lower case + dict.add('a', "L(4)2,2.L(5)1,7") + dict.add('a', "L(4)2,2.L(5)0,8") + dict.add('a', "L(4)2,2.S(5)0,8") + dict.add('b', "S(3)1,7.R(7)6,3") + dict.add('c', "L(4)2,8", top='C') + dict.add('d', "L(4)5,2.S(5)1,7") + dict.add('d', "L(4)5,2.L(5)0,8") + dict.add('e', "S(4)3,5.L(4)5,8") + dict.add('e', "L(4)3,8") + dict.add('f', "L(4)2,6", top='F') + dict.add('f', "S(1)5,3.S(3)1,7", top='F') + dict.add('g', "L(1)2,2.R(4)1,6") + dict.add('h', "S(3)1,7.R(7)6,8") + dict.add('h', "L(3)0,5.R(7)6,8") + dict.add('i', "S(4)1,7", top='I', bot='1') + dict.add('j', "R(4)1,6", top='J') + dict.add('k', "L(3)0,5.L(7)2,8") + dict.add('k', "L(4)0,5.R(7)6,6.L(7)1,8") + dict.add('l', "L(4)0,8", top='L') + dict.add('l', "S(4)0,8", top='L') + dict.add('l', "S(3)1,7.S(7)3,5", top='L') + dict.add('m', "S(3)1,7.R(3)6,8.R(5)6,8") + dict.add('m', "L(3)0,2.R(3)6,8.R(5)6,8") + dict.add('n', "S(3)1,7.R(4)6,8") + dict.add('o', "L(4)1,1", top='O', bot='0') + dict.add('p', "S(3)1,7.R(4)6,3") + dict.add('q', "L(1)2,2.L(5)1,5") + dict.add('q', "L(1)2,2.S(5)1,7.R(8)6,2") + dict.add('q', "L(1)2,2.S(5)1,7.S(5)1,7") + # FIXME this double 1,7 is due to a gentle where the + # second looks like a line because it is narrow.?? + dict.add('r', "S(3)1,7.R(4)6,2") + dict.add('s', "L(1)2,7.R(7)1,6", top='S', bot='5') + dict.add('s', "L(5)1,8.R(7)2,3", top='S', bot='5') + dict.add('t', "R(4)0,8", top='T', bot='7') + dict.add('t', "S(1)3,5.S(5)1,7", top='T', bot='7') + dict.add('u', "L(4)0,2.S(5)1,7") + dict.add('v', "L(4)0,2.L(2)0,2") + dict.add('w', "L(3)0,2.L(5)0,2", top='W') + dict.add('w', "L(3)0,5.R(7)6,8.L(5)3,2", top='W') + dict.add('w', "L(3)0,5.L(5)3,2", top='W') + dict.add('x', "L(4)0,6", top='X') + dict.add('y', "L(1)0,2.R(5)4,6", top='Y') # if curved + dict.add('y', "L(1)0,2.S(5)2,7", top='Y') + dict.add('z', "R(4)0,6.L(4)2,8", top='Z', bot='2') + + # Digits + dict.add('0', "L(4)7,7") + dict.add('0', "R(4)7,7") + dict.add('1', "S(4)7,1") + dict.add('2', "R(4)0,6.S(7)3,5") + dict.add('2', "R(4)3,6.L(4)2,8") + dict.add('3', "R(1)0,6.R(7)1,6") + dict.add('4', "L(4)7,5") + dict.add('5', "L(1)2,6.R(7)0,3") + dict.add('5', "L(1)2,6.L(4)0,8.R(7)0,3") + dict.add('6', "L(4)2,3") + dict.add('7', "S(1)3,5.R(4)1,6") + dict.add('7', "R(4)0,6") + dict.add('7', "R(4)0,7") + dict.add('8', "L(4)2,8.R(4)4,2.L(3)6,1") + dict.add('8', "L(1)2,8.R(7)2,0.L(1)6,1") + dict.add('8', "L(0)2,6.R(7)0,1.L(2)6,0") + dict.add('8', "R(4)2,6.L(4)4,2.R(5)8,1") + dict.add('9', "L(1)2,2.S(5)1,7") + + dict.add(' ', "S(4)3,5") + dict.add('', "S(4)5,3") + dict.add('-', "S(4)3,5.S(4)5,3") + dict.add('_', "S(4)3,5.S(4)5,3.S(4)3,5") + dict.add("", "S(4)5,3.S(3)3,5") + dict.add("","S(4)3,5.S(5)5,3") + dict.add("", "S(4)2,6") + + +class DictSegment: + # Each segment has four elements: + # direction: Right Straight Left (R=cw, L=ccw) + # location: 0-8. + # start: 0-8 + # finish: 0-8 + # Segments match if the difference at each element + # is 0, 1, or 3 (RSL coded as 012) + # A difference of 1 required both to be same / 3 + # On a match, return number of 0s + # On non-match, return -1 + def __init__(self, str): + # D(L)S,R + # 0123456 + self.e = [0,0,0,0] + if len(str) != 7: + raise ValueError + if str[1] != '(' or str[3] != ')' or str[5] != ',': + raise ValueError + if str[0] == 'R': + self.e[0] = 0 + elif str[0] == 'L': + self.e[0] = 2 + elif str[0] == 'S': + self.e[0] = 1 + else: + raise ValueError + + self.e[1] = int(str[2]) + self.e[2] = int(str[4]) + self.e[3] = int(str[6]) + + def match(self, other): + cnt = 0 + for i in range(0,4): + diff = abs(self.e[i] - other.e[i]) + if diff == 0: + cnt += 1 + elif diff == 3: + pass + elif diff == 1 and (self.e[i]/3 == other.e[i]/3): + pass + else: + return -1 + return cnt + +class DictPattern: + # A Dict Pattern is a list of segments. + # A parsed pattern matches a dict pattern if + # the are the same nubmer of segments and they + # all match. The value of the match is the sum + # of the individual matches. + # A DictPattern is printers as segments joined by periods. + # + def __init__(self, str): + self.segs = map(DictSegment, str.split(".")) + def match(self,other): + if len(self.segs) != len(other.segs): + return -1 + cnt = 0 + for i in range(0,len(self.segs)): + m = self.segs[i].match(other.segs[i]) + if m < 0: + return m + cnt += m + return cnt + + +class Dictionary: + # The dictionary hold all the pattern for symbols and + # performs lookup + # Each pattern in the directionary can be associated + # with 3 symbols. One when drawing in middle of screen, + # one for top of screen, one for bottom. + # Often these will all be the same. + # This allows e.g. s and S to have the same pattern in different + # location on the touchscreen. + # A match requires a unique entry with a match that is better + # than any other entry. + # + def __init__(self): + self.dict = [] + def add(self, sym, pat, top = None, bot = None): + if top == None: top = sym + if bot == None: bot = sym + self.dict.append((DictPattern(pat), sym, top, bot)) + + def _match(self, p): + max = -1 + val = None + for (ptn, sym, top, bot) in self.dict: + cnt = ptn.match(p) + if cnt > max: + max = cnt + val = (sym, top, bot) + elif cnt == max: + val = None + return val + + def match(self, str, pos = "mid"): + p = DictPattern(str) + m = self._match(p) + if m == None: + return m + (mid, top, bot) = self._match(p) + if pos == "top": return top + if pos == "bot": return bot + return mid + + +class Point: + # This represents a point in the path and all the points leading + # up to it. It allows us to find the direction and curvature from + # one point to another + # We store x,y, and sum/cnt of points so far + def __init__(self,x,y) : + self.xsum = x + self.ysum = y + self.x = x + self.y = y + self.cnt = 1 + + def copy(self): + n = Point(0,0) + n.xsum = self.xsum + n.ysum = self.ysum + n.x = self.x + n.y = self.y + n.cnt = self.cnt + return n + + def add(self,x,y): + if self.x == x and self.y == y: + return + self.x = x + self.y = y + self.xsum += x + self.ysum += y + self.cnt += 1 + + def xlen(self,p): + return abs(self.x - p.x) + def ylen(self,p): + return abs(self.y - p.y) + def sqlen(self,p): + x = self.x - p.x + y = self.y - p.y + return x*x + y*y + + def xdir(self,p): + if self.x > p.x: + return 1 + if self.x < p.x: + return -1 + return 0 + def ydir(self,p): + if self.y > p.y: + return 1 + if self.y < p.y: + return -1 + return 0 + def curve(self,p): + if self.cnt == p.cnt: + return 0 + x1 = p.x ; y1 = p.y + (x2,y2) = self.meanpoint(p) + x3 = self.x; y3 = self.y + + curve = (y3-y1)*(x2-x1) - (y2-y1)*(x3-x1) + curve = curve * 100 / ((y3-y1)*(y3-y1) + + (x3-x1)*(x3-x1)) + if curve > 6: + return 1 + if curve < -6: + return -1 + return 0 + + def Vcurve(self,p): + if self.cnt == p.cnt: + return 0 + x1 = p.x ; y1 = p.y + (x2,y2) = self.meanpoint(p) + x3 = self.x; y3 = self.y + + curve = (y3-y1)*(x2-x1) - (y2-y1)*(x3-x1) + curve = curve * 100 / ((y3-y1)*(y3-y1) + + (x3-x1)*(x3-x1)) + return curve + + def meanpoint(self,p): + x = (self.xsum - p.xsum) / (self.cnt - p.cnt) + y = (self.ysum - p.ysum) / (self.cnt - p.cnt) + return (x,y) + + def is_sharp(self,A,C): + # Measure the cosine at self between A and C + # as A and C could be curve, we take the mean point on + # self.A and self.C as the points to find cosine between + (ax,ay) = self.meanpoint(A) + (cx,cy) = self.meanpoint(C) + a = ax-self.x; b=ay-self.y + c = cx-self.x; d=cy-self.y + x = a*c + b*d + y = a*d - b*c + h = math.sqrt(x*x+y*y) + if h > 0: + cs = x*1000/h + else: + cs = 0 + return (cs > 900) + +class BBox: + # a BBox records min/max x/y of some Points and + # can subsequently report row, column, pos of each point + # can also locate one bbox in another + + def __init__(self, p): + self.minx = p.x + self.maxx = p.x + self.miny = p.y + self.maxy = p.y + + def width(self): + return self.maxx - self.minx + def height(self): + return self.maxy - self.miny + + def add(self, p): + if p.x > self.maxx: + self.maxx = p.x + if p.x < self.minx: + self.minx = p.x + + if p.y > self.maxy: + self.maxy = p.y + if p.y < self.miny: + self.miny = p.y + def finish(self, div = 3): + # if aspect ratio is bad, we adjust max/min accordingly + # before setting [xy][12]. We don't change self.min/max + # as they are used to place stroke in bigger bbox. + # Normally divisions are at 1/3 and 2/3. They can be moved + # by setting div e.g. 2 = 1/2 and 1/2 + (minx,miny,maxx,maxy) = (self.minx,self.miny,self.maxx,self.maxy) + if (maxx - minx) * 3 < (maxy - miny) * 2: + # too narrow + mid = int((maxx + minx)/2) + halfwidth = int ((maxy - miny)/3) + minx = mid - halfwidth + maxx = mid + halfwidth + if (maxy - miny) * 3 < (maxx - minx) * 2: + # too wide + mid = int((maxy + miny)/2) + halfheight = int ((maxx - minx)/3) + miny = mid - halfheight + maxy = mid + halfheight + + div1 = div - 1 + self.x1 = int((div1*minx + maxx)/div) + self.x2 = int((minx + div1*maxx)/div) + self.y1 = int((div1*miny + maxy)/div) + self.y2 = int((miny + div1*maxy)/div) + + def row(self, p): + # 0, 1, 2 - top to bottom + if p.y <= self.y1: + return 0 + if p.y < self.y2: + return 1 + return 2 + def col(self, p): + if p.x <= self.x1: + return 0 + if p.x < self.x2: + return 1 + return 2 + def box(self, p): + # 0 to 9 + return self.row(p) * 3 + self.col(p) + + def relpos(self,b): + # b is a box within self. find location 0-8 + if b.maxx < self.x2 and b.minx < self.x1: + x = 0 + elif b.minx > self.x1 and b.maxx > self.x2: + x = 2 + else: + x = 1 + if b.maxy < self.y2 and b.miny < self.y1: + y = 0 + elif b.miny > self.y1 and b.maxy > self.y2: + y = 2 + else: + y = 1 + return y*3 + x + + +def different(*args): + cur = 0 + for i in args: + if cur != 0 and i != 0 and cur != i: + return True + if cur == 0: + cur = i + return False + +def maxcurve(*args): + for i in args: + if i != 0: + return i + return 0 + +class PPath: + # a PPath refines a list of x,y points into a list of Points + # The Points mark out segments which end at significant Points + # such as inflections and reversals. + + def __init__(self, x,y): + + self.start = Point(x,y) + self.mid = Point(x,y) + self.curr = Point(x,y) + self.list = [ self.start ] + + def add(self, x, y): + self.curr.add(x,y) + + if ( (abs(self.mid.xdir(self.start) - self.curr.xdir(self.mid)) == 2) or + (abs(self.mid.ydir(self.start) - self.curr.ydir(self.mid)) == 2) or + (abs(self.curr.Vcurve(self.start))+2 < abs(self.mid.Vcurve(self.start)))): + pass + else: + self.mid = self.curr.copy() + + if self.curr.xlen(self.mid) > 4 or self.curr.ylen(self.mid) > 4: + self.start = self.mid.copy() + self.list.append(self.start) + self.mid = self.curr.copy() + + def close(self): + self.list.append(self.curr) + + def get_sectlist(self): + if len(self.list) <= 2: + return [[0,self.list]] + l = [] + A = self.list[0] + B = self.list[1] + s = [A,B] + curcurve = B.curve(A) + for C in self.list[2:]: + cabc = C.curve(A) + cab = B.curve(A) + cbc = C.curve(B) + if B.is_sharp(A,C) and not different(cabc, cab, cbc, curcurve): + # B is too pointy, must break here + l.append([curcurve, s]) + s = [B, C] + curcurve = cbc + elif not different(cabc, cab, cbc, curcurve): + # all happy + s.append(C) + if curcurve == 0: + curcurve = maxcurve(cab, cbc, cabc) + elif not different(cabc, cab, cbc) : + # gentle inflection along AB + # was: AB goes in old and new section + # now: AB only in old section, but curcurve + # preseved. + l.append([curcurve,s]) + s = [A, B, C] + curcurve =maxcurve(cab, cbc, cabc) + else: + # Change of direction at B + l.append([curcurve,s]) + s = [B, C] + curcurve = cbc + + A = B + B = C + l.append([curcurve,s]) + + return l + + def remove_shorts(self, bbox): + # in self.list, if a point is close to the previous point, + # remove it. + if len(self.list) <= 2: + return + w = bbox.width()/10 + h = bbox.height()/10 + n = [self.list[0]] + leng = w*h*2*2 + for p in self.list[1:]: + l = p.sqlen(n[-1]) + if l > leng: + n.append(p) + self.list = n + + def text(self): + # OK, we have a list of points with curvature between. + # want to divide this into sections. + # for each 3 consectutive points ABC curve of ABC and AB and BC + # If all the same, they are all in a section. + # If not B starts a new section and the old ends on B or C... + BB = BBox(self.list[0]) + for p in self.list: + BB.add(p) + BB.finish() + self.bbox = BB + self.remove_shorts(BB) + sectlist = self.get_sectlist() + t = "" + for c, s in sectlist: + if c > 0: + dr = "R" # clockwise is to the Right + elif c < 0: + dr = "L" # counterclockwise to the Left + else: + dr = "S" # straight + bb = BBox(s[0]) + for p in s: + bb.add(p) + bb.finish() + # If all points are in some row or column, then + # line is S + rwdiff = False; cldiff = False + rw = bb.row(s[0]); cl=bb.col(s[0]) + for p in s: + if bb.row(p) != rw: rwdiff = True + if bb.col(p) != cl: cldiff = True + if not rwdiff or not cldiff: dr = "S" + + t1 = dr + t1 += "(%d)" % BB.relpos(bb) + t1 += "%d,%d" % (bb.box(s[0]), bb.box(s[-1])) + t += t1 + '.' + return t[:-1] + + + +def page_cmp(a,b): + if a.lower() < b.lower(): + return -1 + if a.lower() > b.lower(): + return 1 + if a < b: + return -1 + if a > b: + return 1 + return 0 + +def inc_name(a): + l = len(a) + while l > 0 and a[l-1] >= '0' and a[l-1] <= '9': + l -= 1 + # a[l:] is the last number + if l == len(a): + # there is no number + return a + ".1" + num = 0 + int(a[l:]) + return a[0:l] + ("%d" % (num+1)) + +class ScribblePad: + + def __init__(self): + window = gtk.Window(gtk.WINDOW_TOPLEVEL) + window.connect("destroy", self.close_application) + window.set_title("ScribblePad") + #window.set_size_request(480,640) + self.window = window + + vb = gtk.VBox() + vb.show() + self.draw_box = vb + + bar = gtk.HBox(True) + bar.set_size_request(-1, 60) + vb.pack_end(bar, expand=False) + bar.show() + + l = gtk.Label('Page Name') + vb.pack_start(l, expand=False) + l.show() + self.name = l + + page = gtk.DrawingArea() + page.set_size_request(480,540) + vb.add(page) + page.show() + ctx = page.get_pango_context() + fd = ctx.get_font_description() + fd.set_absolute_size(25*pango.SCALE) + page.modify_font(fd) + + l.modify_font(fd) + + dflt = gtk.widget_get_default_style() + fd = dflt.font_desc + fd.set_absolute_size(25*pango.SCALE) + + self.pixbuf = None + self.width = 0 + self.height = 0 + self.need_redraw = True + self.zoomx = None; self.zoomy = None + + # Now the widgets: + + done = gtk.Button("Done"); done.show() + colbtn = gtk.Button("colour"); colbtn.show() + undo = gtk.Button('Undo') ; undo.show() + redo = gtk.Button('Redo') ; redo.show() + + done.child.modify_font(fd) + colbtn.child.modify_font(fd) + undo.child.modify_font(fd) + redo.child.modify_font(fd) + + bar.add(done) + bar.add(colbtn) + bar.add(undo) + bar.add(redo) + + colbtn.connect("clicked", self.colour_change) + undo.connect("clicked", self.undo) + redo.connect("clicked", self.redo) + done.connect("clicked", self.done) + + self.col_align = colbtn + self.undo_join = undo + self.redo_rename = redo + + self.page = page + self.colbtn = colbtn + self.line = None + self.lines = [] + self.hist = [] # undo history + self.selecting = False + self.textcurs = 0 + self.textstr = None + self.textpos = None + + + page.connect("button_press_event", self.press) + page.connect("button_release_event", self.release) + page.connect("motion_notify_event", self.motion) + page.connect("expose-event", self.refresh) + page.connect("configure-event", self.reconfigure) + page.connect("key_press_event", self.type) + page.set_events(gtk.gdk.EXPOSURE_MASK + | gtk.gdk.STRUCTURE_MASK + | gtk.gdk.BUTTON_PRESS_MASK + | gtk.gdk.BUTTON_RELEASE_MASK + | gtk.gdk.KEY_PRESS_MASK + | gtk.gdk.KEY_RELEASE_MASK + | gtk.gdk.POINTER_MOTION_MASK + | gtk.gdk.POINTER_MOTION_HINT_MASK) + page.set_property('can-focus', True) + + + # Now create the index window + # A listselect of all the pages, with a row of buttons: + # open delete new undelete + ls = listselect.ListSelect(center = False) + self.listsel = ls + + ls.connect('selected', self.page_select) + ls.set_colour('page','blue') + ls.set_zoom(38) + ls.show() + + vb = gtk.VBox() + vb.show() + self.list_box = vb + + vb.add(ls) + bar = gtk.HBox(True); bar.show() + bar.set_size_request(-1, 60) + b= gtk.Button("Open"); b.child.modify_font(fd); b.show(); bar.add(b); b.connect('clicked', self.open_page) + b= gtk.Button("Del"); b.child.modify_font(fd); b.show(); bar.add(b); b.connect('clicked', self.del_page) + b= gtk.Button("New"); b.child.modify_font(fd); b.show(); bar.add(b); b.connect('clicked', self.new_page) + b= gtk.Button("Undelete"); b.child.modify_font(fd); b.show(); bar.add(b); b.connect('clicked', self.undelete_pages) + + vb.pack_end(bar, expand=False) + + window.add(self.draw_box) + + window.set_default_size(480,640) + + window.show() + + + if 'HOME' in os.environ: + home = os.environ['HOME'] + else: + home = "" + if home == "" or home == "/": + home = "/home/root" + self.page_dir = home + '/Pages' + self.page_dir = '/data/Pages' + self.load_pages() + + colourmap = page.get_colormap() + self.colourmap = {} + self.colnames = [ 'black', 'red', 'blue', 'green', 'purple', 'pink', 'yellow' ] + for col in self.colnames: + c = gtk.gdk.color_parse(col) + gc = page.window.new_gc() + gc.line_width = 2 + gc.set_foreground(colourmap.alloc_color(c)) + self.colourmap[col] = gc + self.reset_colour = False + + self.colour_textmode = self.colourmap['blue'] + + self.colourname = "black" + self.colour = self.colourmap['black'] + self.colbtn.child.set_text('black') + self.bg = page.get_style().bg_gc[gtk.STATE_NORMAL] + + self.dict = Dictionary() + LoadDict(self.dict) + self.textstr = None + + + ctx = page.get_pango_context() + fd = ctx.get_font_description() + met = ctx.get_metrics(fd) + self.lineheight = (met.get_ascent() + met.get_descent()) / pango.SCALE + self.lineascent = met.get_ascent() / pango.SCALE + + self.timeout = None + + window.remove(self.draw_box) + window.add(self.list_box) + + + def close_application(self, widget): + self.save_page() + gtk.main_quit() + + def load_pages(self): + try: + os.mkdir(self.page_dir) + except: + pass + self.names = os.listdir(self.page_dir) + if len(self.names) == 0: + self.names.append("1") + self.names.sort(page_cmp) + self.pages = {} + self.pagenum = 0 + self.load_page() + self.update_list() + + def update_list(self): + l = [] + for p in self.names: + l.append([p,'page']) + self.listsel.list = l + self.listsel.list_changed() + self.listsel.select(self.pagenum) + return + + def page_select(self, list, item): + if item == None: + return + self.pagenum = item + + def open_page(self, b): + self.load_page() + self.window.remove(self.list_box) + self.window.add(self.draw_box) + + def done(self, b): + self.flush_text() + self.save_page() + self.window.remove(self.draw_box) + self.window.add(self.list_box) + + def del_page(self, b): + pass + + def new_page(self, b): + newname = self.choose_unique(self.names[self.pagenum]) + self.names = self.names[0:self.pagenum+1] + [ newname ] + \ + self.names[self.pagenum+1:] + self.pagenum += 1; + self.update_list() + self.listsel.select(self.pagenum) + + return + + def undelete_pages(self, b): + pass + + + def type(self, c, ev): + if ev.keyval == 65288: + self.add_sym('') + elif ev.string == '\r': + self.add_sym('') + else: + self.add_sym(ev.string) + + def press(self, c, ev): + # Start a new line + if self.timeout: + gobject.source_remove(self.timeout) + self.timeout = None + self.taptime = time.time() + self.movetext = None + c.grab_focus() + self.movetimeout = None + self.selecting = False + if self.selection: + self.selection = None + self.need_redraw = True + self.name_buttons() + self.redraw() + + self.line = [ self.colourname, [int(ev.x), int(ev.y)] ] + return + def release(self, c, ev): + if self.movetimeout: + gobject.source_remove(self.movetimeout) + self.movetimeout = None + + if self.movetext: + self.movetext = None + self.line = None + self.need_redraw = True + self.redraw() + return + if self.line == None: + return + + if self.selecting: + self.selecting = False + self.line = None + return + if self.timeout == None: + self.timeout = gobject.timeout_add(20*1000, self.tick) + + if len(self.line) == 2: + # just set a cursor + need_redraw = ( self.textstr != None) + oldpos = None + if not self.textstr: + oldpos = self.textpos + self.flush_text() + if need_redraw: + self.redraw() + (lineno,index) = self.find_text(self.line[1]) + + if lineno == None: + # new text, + pos = self.align_text(self.line[1]) + if oldpos and abs(pos[0]-oldpos[0]) < 40 and \ + abs(pos[1] - oldpos[1]) < 40: + # turn of text mode + self.flush_text() + self.line = None + self.need_redraw = True + self.setlabel() + return + self.textpos = pos + self.textstr = "" + self.textcurs = 0 + # draw the cursor + self.draw_text(pos, self.colour, "", 0) + self.line = None + else: + # clicked inside an old text. + # shuffle it to the top, open it, edit. + ln = self.lines[lineno] + self.lines = self.lines[:lineno] + self.lines[lineno+1:] + self.textpos = ln[1] + self.textstr = ln[2] + if ln[0] in self.colourmap: + self.colourname = ln[0] + else: + self.colourname = "black" + self.colbtn.child.set_text(self.colourname) + self.colour = self.colourmap[self.colourname] + self.textcurs = index + 1 + self.need_redraw = True + self.redraw() + self.setlabel() + self.line = None + return + if self.textstr != None: + sym = self.getsym() + if sym: + self.add_sym(sym) + else: + self.redraw() + self.line = None + self.reset_colour = True + return + + self.lines.append(self.line) + self.line = None + self.reset_colour = True + self.need_redraw = True + return + def motion(self, c, ev): + if self.line: + if ev.is_hint: + x, y, state = ev.window.get_pointer() + else: + x = ev.x + y = ev.y + x = int(x) + y = int(y) + prev = self.line[-1] + if not self.movetext and abs(prev[0] - x) < 10 and abs(prev[1] - y) < 10: + return + if not self.movetext and len(self.line) == 2 and time.time() - self.taptime > 0.5: + self.flush_text() + (lineno, index) = self.find_text(prev) + if lineno != None: + self.movetext = self.lines[lineno] + self.moveoffset = [prev[0]-self.movetext[1][0], prev[1]-self.movetext[1][1]] + if self.movetext: + self.movetext[1] = [x-self.moveoffset[0],y-self.moveoffset[1]] + self.need_redraw = True + self.redraw() + return + + if self.movetimeout: + gobject.source_remove(self.movetimeout) + self.movetimeout = gobject.timeout_add(650, self.movetick) + + if self.textstr != None: + c.window.draw_line(self.colour_textmode, prev[0],prev[1],x,y) + else: + c.window.draw_line(self.colour, prev[0],prev[1],x,y) + self.line.append([x,y]) + return + + def movetick(self): + # longish pause while drawing + self.flush_text() + self.selecting = True + self.need_redraw = True + self.redraw() + + def tick(self): + # nothing for 20 seconds, flush the page + self.save_page() + gobject.source_remove(self.timeout) + self.timeout = None + + def find_text(self, pos): + x = pos[0]; y = pos[1] + self.lineascent + for i in range(0, len(self.lines)): + p = self.lines[i] + if type(p[2]) != str: + continue + if x >= p[1][0] and y >= p[1][1] and y < p[1][1] + self.lineheight: + # could be this line - check more precisely + layout = self.page.create_pango_layout(p[2]+' ') + (ink, log) = layout.get_pixel_extents() + (ex,ey,ew,eh) = log + if x < p[1][0] + ex or x > p[1][0] + ex + ew or \ + y < p[1][1] + ey or \ + y > p[1][1] + ey + self.lineheight : + continue + # OK, it is in this one. Find out where. + (index, gr) = layout.xy_to_index((x - p[1][0] - ex) * pango.SCALE, + (y - p[1][1] - ey - self.lineheight) * pango.SCALE) + if index >= len(p[2]): + index = len(p[2])-1 + return (i, index) + return (None, None) + + def align_text(self, pos): + # align pos to existing text. + # if pos is near one-line-past a previous text, move the exactly + # one-line-past + x = pos[0]; y = pos[1] + self.lineascent + for l in self.lines: + if type(l[2]) != str: + continue + if abs(x - l[1][0]) > self.lineheight: + continue + if abs(y - (l[1][1] + self.lineheight)) > self.lineheight: + continue + return [ l[1][0], l[1][1] + self.lineheight ] + return pos + + def flush_text(self): + if self.textstr == None: + self.textpos = None + return + self.setlabel() + if len(self.textstr) == 0: + self.textstr = None + self.textpos = None + return + l = [self.colourname, self.textpos, self.textstr] + self.lines.append(l) + self.need_redraw = True + self.textstr = None + self.textpos = None + + def draw_text(self, pos, colour, str, cursor = None, drawable = None, selected=False): + if drawable == None: + drawable = self.page.window + layout = self.page.create_pango_layout(str) + if self.zoomx != None: + xs = 2; xo = -self.zoomx + ys = 2; yo = -self.zoomy + else: + xs = 1 ; xo = 0 + ys = 1 ; yo = 0 + (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents() + if len(pos) == 2 or pos[2] != ew or pos[3] != eh: + while len(pos) > 2: + pos.pop() + pos.append(ew) + pos.append(eh) + if selected: + drawable.draw_rectangle(self.colourmap['yellow'], True, + ex+pos[0], ey+pos[1]-self.lineascent, ew, eh) + drawable.draw_layout(colour, pos[0]*xs+xo, + (pos[1] - self.lineascent)*ys + yo, + layout) + if cursor != None: + (strong,weak) = layout.get_cursor_pos(cursor) + (x,y,width,height) = strong + x = pos[0] + x / pango.SCALE + y = pos[1] + drawable.draw_line(self.colour_textmode, + x*xs+xo,y*ys+yo, (x-3)*xs+xo, (y+3)*ys+yo) + drawable.draw_line(self.colour_textmode, + x*xs+xo,y*ys+yo, (x+3)*xs+xo, (y+3)*ys+yo) + + def add_sym(self, sym): + if self.textstr == None: + return + if sym == "": + if self.textcurs > 0: + self.textstr = self.textstr[0:self.textcurs-1]+ \ + self.textstr[self.textcurs:] + self.textcurs -= 1 + elif sym == "": + if self.textcurs > 0: + self.textcurs -= 1 + elif sym == "": + if self.textcurs < len(self.textstr): + self.textcurs += 1 + elif sym == "": + tail = self.textstr[self.textcurs:] + self.textstr = self.textstr[:self.textcurs] + oldpos = self.textpos + self.flush_text() + self.textcurs = len(tail) + self.textstr = tail + self.textpos = [ oldpos[0], oldpos[1] + + self.lineheight ] + else: + self.textstr = self.textstr[0:self.textcurs] + sym + \ + self.textstr[self.textcurs:] + self.textcurs += 1 + self.redraw() + + + def getsym(self): + alloc = self.page.get_allocation() + pagebb = BBox(Point(0,0)) + pagebb.add(Point(alloc.width, alloc.height)) + pagebb.finish(div = 2) + + p = PPath(self.line[1][0], self.line[1][1]) + for pp in self.line[1:]: + p.add(pp[0], pp[1]) + p.close() + patn = p.text() + pos = pagebb.relpos(p.bbox) + tpos = "mid" + if pos < 3: + tpos = "top" + if pos >= 6: + tpos = "bot" + sym = self.dict.match(patn, tpos) + if sym == None: + print "Failed to match pattern:", patn + return sym + + def refresh(self, area, ev): + self.redraw() + + def setlabel(self): + if self.textstr == None: + self.name.set_label('Page: ' + self.names[self.pagenum] + ' (draw)') + else: + self.name.set_label('Page: ' + self.names[self.pagenum] + ' (text)') + + def name_buttons(self): + if self.selection: + self.col_align.child.set_text('Align') + self.undo_join.child.set_text('Join') + self.redo_rename.child.set_text('Rename') + else: + self.col_align.child.set_text(self.colourname) + self.undo_join.child.set_text('Undo') + self.redo_rename.child.set_text('Redo') + + def redraw(self): + self.setlabel() + + if self.need_redraw: + self.draw_buf() + self.page.window.draw_drawable(self.bg, self.pixbuf, 0, 0, 0, 0, + self.width, self.height) + + if self.textstr != None: + self.draw_text(self.textpos, self.colour, self.textstr, + self.textcurs) + + return + def draw_buf(self): + if self.pixbuf == None: + alloc = self.page.get_allocation() + self.pixbuf = gtk.gdk.Pixmap(self.page.window, alloc.width, alloc.height) + self.width = alloc.width + self.height = alloc.height + + self.pixbuf.draw_rectangle(self.bg, True, 0, 0, + self.width, self.height) + if self.zoomx != None: + xs = 2; xo = -self.zoomx + ys = 2; yo = -self.zoomy + else: + xs = 1 ; xo = 0 + ys = 1 ; yo = 0 + self.selection = [] + for l in self.lines: + if l[0] in self.colourmap: + col = self.colourmap[l[0]] + else: + col = self.colourmap['black'] + st = l[1] + if type(l[2]) == list: + for p in l[2:]: + self.pixbuf.draw_line(col, st[0]*xs + xo, st[1]*ys + yo, + p[0]*xs + xo,p[1]* ys + yo) + st = p + if type(l[2]) == str: + # if this text is 'near' the current line, make it red + selected = False + if self.selecting and self.line and len(st) == 4: + for p in self.line[1:]: + if p[0] > st[0] and \ + p[0] < st[0] + st[2] and \ + p[1] > st[1] - self.lineascent and \ + p[1] < st[1] - self.lineascent + st[3]: + selected = True + break + if selected: + self.selection.append(l) + self.draw_text(st, col, l[2], drawable = self.pixbuf, + selected = selected) + self.need_redraw = False + self.name_buttons() + + + def reconfigure(self, w, ev): + alloc = w.get_allocation() + if self.pixbuf == None: + return + if alloc.width != self.width or alloc.height != self.height: + self.pixbuf = None + self.need_redraw = True + + + def colour_change(self,t): + if self.selection: + # button is 'join' not 'colour' + return self.realign(t) + + if self.reset_colour and self.colourname != 'black': + next = 'black' + else: + next = 'black' + prev = '' + for c in self.colnames: + if self.colourname == prev: + next = c + prev = c + self.reset_colour = False + self.colourname = next + self.colour = self.colourmap[next] + t.child.set_text(next) + if self.textstr: + self.draw_text(self.textpos, self.colour, self.textstr, + self.textcurs) + + return + def text_change(self,t): + self.flush_text() + return + def undo(self,b): + if self.selection: + return self.join(b) + + if len(self.lines) == 0: + return + self.hist.append(self.lines.pop()) + self.need_redraw = True + self.redraw() + return + def redo(self,b): + if self.selection: + self.rename(self.selection[0][2]) + return + if len(self.hist) == 0: + return + self.lines.append(self.hist.pop()) + self.need_redraw = True + self.redraw() + return + def choose_unique(self, newname): + while newname in self.names: + new2 = inc_name(newname) + if new2 not in self.names: + newname = new2 + elif (newname + ".1") not in self.names: + newname = newname + ".1" + else: + newname = newname + ".0.1" + + return newname + def delete(self,b): + # hack + if self.selection: + return self.join(b) + self.flush_text() + if len(self.names) <= 1: + return + if len(self.lines) > 0: + return + self.save_page() + nm = self.names[self.pagenum] + if nm in self.pages: + del self.pages[nm] + self.names = self.names[0:self.pagenum] + self.names[self.pagenum+1:] + if self.pagenum >= len(self.names): + self.pagenum -= 1 + self.load_page() + self.need_redraw = True + self.redraw() + + return + + def cmplines(self, a,b): + pa = a[1] + pb = b[1] + if pa[1] != pb[1]: + return pa[1] - pb[1] + return pa[0] - pb[0] + + def realign(self, b): + self.selection.sort(self.cmplines) + x = self.selection[0][1][0] + y = self.selection[0][1][1] + for i in range(len(self.selection)): + self.selection[i][1][0] = x + self.selection[i][1][1] = y + y += self.lineheight + self.need_redraw = True + self.redraw() + + def join(self, b): + self.selection.sort(self.cmplines) + txt = "" + for i in range(len(self.selection)): + if txt: + txt = txt + ' ' + self.selection[i][2] + else: + txt = self.selection[i][2] + self.selection[i][2] = None + self.selection[0][2] = txt + i = 0; + while i < len(self.lines): + if len(self.lines[i]) > 2 and self.lines[i][2] == None: + self.lines = self.lines[:i] + self.lines[i+1:] + else: + i += 1 + self.need_redraw = True + self.redraw() + + + def rename(self, newname): + # Rename current page and rename the file + if self.names[self.pagenum] == newname: + return + self.save_page() + newname = self.choose_unique(newname) + oldpath = self.page_dir + "/" + self.names[self.pagenum] + newpath = self.page_dir + "/" + newname + try : + os.rename(oldpath, newpath) + self.names[self.pagenum] = newname + self.names.sort(page_cmp) + self.pagenum = self.names.index(newname) + self.setlabel() + except: + pass + self.update_list() + + def setname(self,b): + if self.textstr: + if len(self.textstr) > 0: + self.rename(self.textstr) + + def clear(self,b): + while len(self.lines) > 0: + self.hist.append(self.lines.pop()) + self.need_redraw = True + self.redraw() + return + + def parseline(self, l): + # string in "", or num,num. ':' separates words + words = l.strip().split(':') + line = [] + for w in words: + if w[0] == '"': + w = w[1:-1] + elif w.find(',') >= 0: + n = w.find(',') + x = int(w[:n]) + y = int(w[n+1:]) + w = [x,y] + line.append(w) + return line + + def load_page(self): + self.need_redraw = True + nm = self.names[self.pagenum] + if nm in self.pages: + self.lines = self.pages[nm] + return + self.lines = []; + try: + f = open(self.page_dir + "/" + self.names[self.pagenum], "r") + except: + f = None + if f: + l = f.readline() + while len(l) > 0: + self.lines.append(self.parseline(l)) + l = f.readline() + f.close() + return + + def save_page(self): + t = self.textstr; tc = self.textcurs + self.flush_text() + self.pages[self.names[self.pagenum]] = self.lines + tosave = self.lines + if t and len(t): + # restore the text + ln = self.lines[-1] + self.lines = self.lines[:-1] + self.textpos = ln[1] + self.textstr = ln[2] + self.textcurs = tc + + fn = self.page_dir + "/" + self.names[self.pagenum] + if len(tosave) == 0: + try: + os.unlink(fn) + except: + pass + return + f = open(fn, "w") + for l in tosave: + start = True + if not l: + continue + for w in l: + if not start: + f.write(":") + start = False + if isinstance(w, str): + f.write('"%s"' % w) + elif isinstance(w, list): + f.write("%d,%d" %( w[0],w[1])) + f.write("\n") + f.close() + +def main(): + gtk.main() + return 0 +if __name__ == "__main__": + ScribblePad() + main() diff --git a/shop/shop.py b/shop/shop.py new file mode 100755 index 0000000..4e3cfc4 --- /dev/null +++ b/shop/shop.py @@ -0,0 +1,2210 @@ +#!/usr/bin/env python + +# +# TO FIX +# - document +# - use separate hand-writing code +# - use separate list-select code + + +import sys, os, time +import pygtk, gtk, pango +import gobject + +########################################################### +# Writing recognistion code +import math + +global place +place = 0 + +def LoadDict(dict): + # Upper case. + # Where they are like lowercase, we either double + # the last stroke (L, J, I) or draw backwards (S, Z, X) + # U V are a special case + + dict.add('A', "R(4)6,8") + dict.add('B', "R(4)6,4.R(7)1,6") + dict.add('B', "R(4)6,4.L(4)2,8.R(7)1,6") + dict.add('B', "S(6)7,1.R(4)6,4.R(7)0,6") + dict.add('C', "R(4)8,2") + dict.add('D', "R(4)6,6") + dict.add('E', "L(1)2,8.L(7)2,8") + # double the stem for F + dict.add('F', "L(4)2,6.S(3)7,1") + dict.add('F', "S(1)5,3.S(3)1,7.S(3)7,1") + + dict.add('G', "L(4)2,5.S(8)1,7") + dict.add('G', "L(4)2,5.R(8)6,8") + # FIXME I need better straight-curve alignment + dict.add('H', "S(3)1,7.R(7)6,8.S(5)7,1") + dict.add('H', "L(3)0,5.R(7)6,8.S(5)7,1") + # capital I is down/up + dict.add('I', "S(4)1,7.S(4)7,1") + + # Capital J has a left/right tail + dict.add('J', "R(4)1,6.S(7)3,5") + + dict.add('K', "L(4)0,2.R(4)6,6.L(4)2,8") + + # Capital L, like J, doubles the foot + dict.add('L', "L(4)0,8.S(7)4,3") + + dict.add('M', "R(3)6,5.R(5)3,8") + dict.add('M', "R(3)6,5.L(1)0,2.R(5)3,8") + + dict.add('N', "R(3)6,8.L(5)0,2") + + # Capital O is CW, but can be CCW in special dict + dict.add('O', "R(4)1,1", bot='0') + + dict.add('P', "R(4)6,3") + dict.add('Q', "R(4)7,7.S(8)0,8") + + dict.add('R', "R(4)6,4.S(8)0,8") + + # S is drawn bottom to top. + dict.add('S', "L(7)6,1.R(1)7,2") + + # Double the stem for capital T + dict.add('T', "R(4)0,8.S(5)7,1") + + # U is L to R, V is R to L for now + dict.add('U', "L(4)0,2") + dict.add('V', "R(4)2,0") + + dict.add('W', "R(5)2,3.L(7)8,6.R(3)5,0") + dict.add('W', "R(5)2,3.R(3)5,0") + + dict.add('X', "R(4)6,0") + + dict.add('Y',"L(1)0,2.R(5)4,6.S(5)6,2") + dict.add('Y',"L(1)0,2.S(5)2,7.S(5)7,2") + + dict.add('Z', "R(4)8,2.L(4)6,0") + + # Lower case + dict.add('a', "L(4)2,2.L(5)1,7") + dict.add('a', "L(4)2,2.L(5)0,8") + dict.add('a', "L(4)2,2.S(5)0,8") + dict.add('b', "S(3)1,7.R(7)6,3") + dict.add('c', "L(4)2,8", top='C') + dict.add('d', "L(4)5,2.S(5)1,7") + dict.add('d', "L(4)5,2.L(5)0,8") + dict.add('e', "S(4)3,5.L(4)5,8") + dict.add('e', "L(4)3,8") + dict.add('f', "L(4)2,6", top='F') + dict.add('f', "S(1)5,3.S(3)1,7", top='F') + dict.add('g', "L(1)2,2.R(4)1,6") + dict.add('h', "S(3)1,7.R(7)6,8") + dict.add('h', "L(3)0,5.R(7)6,8") + dict.add('i', "S(4)1,7", top='I', bot='1') + dict.add('j', "R(4)1,6", top='J') + dict.add('k', "L(3)0,5.L(7)2,8") + dict.add('k', "L(4)0,5.R(7)6,6.L(7)1,8") + dict.add('l', "L(4)0,8", top='L') + dict.add('l', "S(3)1,7.S(7)3,5", top='L') + dict.add('m', "S(3)1,7.R(3)6,8.R(5)6,8") + dict.add('m', "L(3)0,2.R(3)6,8.R(5)6,8") + dict.add('n', "S(3)1,7.R(4)6,8") + dict.add('o', "L(4)1,1", top='O', bot='0') + dict.add('p', "S(3)1,7.R(4)6,3") + dict.add('q', "L(1)2,2.L(5)1,5") + dict.add('q', "L(1)2,2.S(5)1,7.R(8)6,2") + dict.add('q', "L(1)2,2.S(5)1,7.S(5)1,7") + # FIXME this double 1,7 is due to a gentle where the + # second looks like a line because it is narrow.?? + dict.add('r', "S(3)1,7.R(4)6,2") + dict.add('s', "L(1)2,7.R(7)1,6", top='S', bot='5') + dict.add('t', "R(4)0,8", top='T', bot='7') + dict.add('t', "S(1)3,5.S(5)1,7", top='T', bot='7') + dict.add('u', "L(4)0,2.S(5)1,7") + dict.add('v', "L(4)0,2.L(2)0,2") + dict.add('w', "L(3)0,2.L(5)0,2", top='W') + dict.add('w', "L(3)0,5.R(7)6,8.L(5)3,2", top='W') + dict.add('w', "L(3)0,5.L(5)3,2", top='W') + dict.add('x', "L(4)0,6", top='X') + dict.add('y', "L(1)0,2.R(5)4,6", top='Y') # if curved + dict.add('y', "L(1)0,2.S(5)2,7", top='Y') + dict.add('z', "R(4)0,6.L(4)2,8", top='Z', bot='2') + + # Digits + dict.add('0', "L(4)7,7") + dict.add('0', "R(4)7,7") + dict.add('1', "S(4)7,1") + dict.add('2', "R(4)0,6.S(7)3,5") + dict.add('2', "R(4)3,6.L(4)2,8") + dict.add('3', "R(1)0,6.R(7)1,6") + dict.add('4', "L(4)7,5") + dict.add('5', "L(1)2,6.R(7)0,3") + dict.add('5', "L(1)2,6.L(4)0,8.R(7)0,3") + dict.add('6', "L(4)2,3") + dict.add('7', "S(1)3,5.R(4)1,6") + dict.add('7', "R(4)0,6") + dict.add('7', "R(4)0,7") + dict.add('8', "L(4)2,8.R(4)4,2.L(3)6,1") + dict.add('8', "L(1)2,8.R(7)2,0.L(1)6,1") + dict.add('8', "L(0)2,6.R(7)0,1.L(2)6,0") + dict.add('8', "R(4)2,6.L(4)4,2.R(5)8,1") + dict.add('9', "L(1)2,2.S(5)1,7") + + dict.add(' ', "S(4)3,5") + dict.add('', "S(4)5,3") + dict.add('-', "S(4)3,5.S(4)5,3") + dict.add('_', "S(4)3,5.S(4)5,3.S(4)3,5") + dict.add("", "S(4)5,3.S(3)3,5") + dict.add("","S(4)3,5.S(5)5,3") + dict.add("", "S(4)7,1.S(1)1,7") # "" + dict.add("","S(4)1,7.S(7)7,1") # "" + dict.add("", "S(4)2,6") + + +class DictSegment: + # Each segment has for elements: + # direction: Right Straight Left (R=cw, L=ccw) + # location: 0-8. + # start: 0-8 + # finish: 0-8 + # Segments match if there difference at each element + # is 0, 1, or 3 (RSL coded as 012) + # A difference of 1 required both to be same / 3 + # On a match, return number of 0s + # On non-match, return -1 + def __init__(self, str): + # D(L)S,R + # 0123456 + self.e = [0,0,0,0] + if len(str) != 7: + raise ValueError + if str[1] != '(' or str[3] != ')' or str[5] != ',': + raise ValueError + if str[0] == 'R': + self.e[0] = 0 + elif str[0] == 'L': + self.e[0] = 2 + elif str[0] == 'S': + self.e[0] = 1 + else: + raise ValueError + + self.e[1] = int(str[2]) + self.e[2] = int(str[4]) + self.e[3] = int(str[6]) + + def match(self, other): + cnt = 0 + for i in range(0,4): + diff = abs(self.e[i] - other.e[i]) + if diff == 0: + cnt += 1 + elif diff == 3: + pass + elif diff == 1 and (self.e[i]/3 == other.e[i]/3): + pass + else: + return -1 + return cnt + +class DictPattern: + # A Dict Pattern is a list of segments. + # A parsed pattern matches a dict pattern if + # the are the same nubmer of segments and they + # all match. The value of the match is the sum + # of the individual matches. + # A DictPattern is printers as segments joined by periods. + # + def __init__(self, str): + self.segs = map(DictSegment, str.split(".")) + def match(self,other): + if len(self.segs) != len(other.segs): + return -1 + cnt = 0 + for i in range(0,len(self.segs)): + m = self.segs[i].match(other.segs[i]) + if m < 0: + return m + cnt += m + return cnt + + +class Dictionary: + # The dictionary hold all the pattern for symbols and + # performs lookup + # Each pattern in the directionary can be associated + # with 3 symbols. One when drawing in middle of screen, + # one for top of screen, one for bottom. + # Often these will all be the same. + # This allows e.g. s and S to have the same pattern in different + # location on the touchscreen. + # A match requires a unique entry with a match that is better + # than any other entry. + # + def __init__(self): + self.dict = [] + def add(self, sym, pat, top = None, bot = None): + if top == None: top = sym + if bot == None: bot = sym + self.dict.append((DictPattern(pat), sym, top, bot)) + + def _match(self, p): + max = -1 + val = None + for (ptn, sym, top, bot) in self.dict: + cnt = ptn.match(p) + if cnt > max: + max = cnt + val = (sym, top, bot) + elif cnt == max: + val = None + return val + + def match(self, str, pos = "mid"): + p = DictPattern(str) + m = self._match(p) + if m == None: + return m + (mid, top, bot) = self._match(p) + if pos == "top": return top + if pos == "bot": return bot + return mid + + +class Point: + # This represents a point in the path and all the points leading + # up to it. It allows us to find the direction and curvature from + # one point to another + # We store x,y, and sum/cnt of points so far + def __init__(self,x,y) : + self.xsum = x + self.ysum = y + self.x = x + self.y = y + self.cnt = 1 + + def copy(self): + n = Point(0,0) + n.xsum = self.xsum + n.ysum = self.ysum + n.x = self.x + n.y = self.y + n.cnt = self.cnt + return n + + def add(self,x,y): + if self.x == x and self.y == y: + return + self.x = x + self.y = y + self.xsum += x + self.ysum += y + self.cnt += 1 + + def xlen(self,p): + return abs(self.x - p.x) + def ylen(self,p): + return abs(self.y - p.y) + def sqlen(self,p): + x = self.x - p.x + y = self.y - p.y + return x*x + y*y + + def xdir(self,p): + if self.x > p.x: + return 1 + if self.x < p.x: + return -1 + return 0 + def ydir(self,p): + if self.y > p.y: + return 1 + if self.y < p.y: + return -1 + return 0 + def curve(self,p): + if self.cnt == p.cnt: + return 0 + x1 = p.x ; y1 = p.y + (x2,y2) = self.meanpoint(p) + x3 = self.x; y3 = self.y + + curve = (y3-y1)*(x2-x1) - (y2-y1)*(x3-x1) + curve = curve * 100 / ((y3-y1)*(y3-y1) + + (x3-x1)*(x3-x1)) + if curve > 6: + return 1 + if curve < -6: + return -1 + return 0 + + def Vcurve(self,p): + if self.cnt == p.cnt: + return 0 + x1 = p.x ; y1 = p.y + (x2,y2) = self.meanpoint(p) + x3 = self.x; y3 = self.y + + curve = (y3-y1)*(x2-x1) - (y2-y1)*(x3-x1) + curve = curve * 100 / ((y3-y1)*(y3-y1) + + (x3-x1)*(x3-x1)) + return curve + + def meanpoint(self,p): + x = (self.xsum - p.xsum) / (self.cnt - p.cnt) + y = (self.ysum - p.ysum) / (self.cnt - p.cnt) + return (x,y) + + def is_sharp(self,A,C): + # Measure the cosine at self between A and C + # as A and C could be curve, we take the mean point on + # self.A and self.C as the points to find cosine between + (ax,ay) = self.meanpoint(A) + (cx,cy) = self.meanpoint(C) + a = ax-self.x; b=ay-self.y + c = cx-self.x; d=cy-self.y + x = a*c + b*d + y = a*d - b*c + h = math.sqrt(x*x+y*y) + if h > 0: + cs = x*1000/h + else: + cs = 0 + return (cs > 900) + +class BBox: + # a BBox records min/max x/y of some Points and + # can subsequently report row, column, pos of each point + # can also locate one bbox in another + + def __init__(self, p): + self.minx = p.x + self.maxx = p.x + self.miny = p.y + self.maxy = p.y + + def width(self): + return self.maxx - self.minx + def height(self): + return self.maxy - self.miny + + def add(self, p): + if p.x > self.maxx: + self.maxx = p.x + if p.x < self.minx: + self.minx = p.x + + if p.y > self.maxy: + self.maxy = p.y + if p.y < self.miny: + self.miny = p.y + def finish(self, div = 3): + # if aspect ratio is bad, we adjust max/min accordingly + # before setting [xy][12]. We don't change self.min/max + # as they are used to place stroke in bigger bbox. + # Normally divisions are at 1/3 and 2/3. They can be moved + # by setting div e.g. 2 = 1/2 and 1/2 + (minx,miny,maxx,maxy) = (self.minx,self.miny,self.maxx,self.maxy) + if (maxx - minx) * 3 < (maxy - miny) * 2: + # too narrow + mid = int((maxx + minx)/2) + halfwidth = int ((maxy - miny)/3) + minx = mid - halfwidth + maxx = mid + halfwidth + if (maxy - miny) * 3 < (maxx - minx) * 2: + # too wide + mid = int((maxy + miny)/2) + halfheight = int ((maxx - minx)/3) + miny = mid - halfheight + maxy = mid + halfheight + + div1 = div - 1 + self.x1 = int((div1*minx + maxx)/div) + self.x2 = int((minx + div1*maxx)/div) + self.y1 = int((div1*miny + maxy)/div) + self.y2 = int((miny + div1*maxy)/div) + + def row(self, p): + # 0, 1, 2 - top to bottom + if p.y <= self.y1: + return 0 + if p.y < self.y2: + return 1 + return 2 + def col(self, p): + if p.x <= self.x1: + return 0 + if p.x < self.x2: + return 1 + return 2 + def box(self, p): + # 0 to 9 + return self.row(p) * 3 + self.col(p) + + def relpos(self,b): + # b is a box within self. find location 0-8 + if b.maxx < self.x2 and b.minx < self.x1: + x = 0 + elif b.minx > self.x1 and b.maxx > self.x2: + x = 2 + else: + x = 1 + if b.maxy < self.y2 and b.miny < self.y1: + y = 0 + elif b.miny > self.y1 and b.maxy > self.y2: + y = 2 + else: + y = 1 + return y*3 + x + + +def different(*args): + cur = 0 + for i in args: + if cur != 0 and i != 0 and cur != i: + return True + if cur == 0: + cur = i + return False + +def maxcurve(*args): + for i in args: + if i != 0: + return i + return 0 + +class PPath: + # a PPath refines a list of x,y points into a list of Points + # The Points mark out segments which end at significant Points + # such as inflections and reversals. + + def __init__(self, x,y): + + self.start = Point(x,y) + self.mid = Point(x,y) + self.curr = Point(x,y) + self.list = [ self.start ] + + def add(self, x, y): + self.curr.add(x,y) + + if ( (abs(self.mid.xdir(self.start) - self.curr.xdir(self.mid)) == 2) or + (abs(self.mid.ydir(self.start) - self.curr.ydir(self.mid)) == 2) or + (abs(self.curr.Vcurve(self.start))+2 < abs(self.mid.Vcurve(self.start)))): + pass + else: + self.mid = self.curr.copy() + + if self.curr.xlen(self.mid) > 4 or self.curr.ylen(self.mid) > 4: + self.start = self.mid.copy() + self.list.append(self.start) + self.mid = self.curr.copy() + + def close(self): + self.list.append(self.curr) + + def get_sectlist(self): + if len(self.list) <= 2: + return [[0,self.list]] + l = [] + A = self.list[0] + B = self.list[1] + s = [A,B] + curcurve = B.curve(A) + for C in self.list[2:]: + cabc = C.curve(A) + cab = B.curve(A) + cbc = C.curve(B) + if B.is_sharp(A,C) and not different(cabc, cab, cbc, curcurve): + # B is too pointy, must break here + l.append([curcurve, s]) + s = [B, C] + curcurve = cbc + elif not different(cabc, cab, cbc, curcurve): + # all happy + s.append(C) + if curcurve == 0: + curcurve = maxcurve(cab, cbc, cabc) + elif not different(cabc, cab, cbc) : + # gentle inflection along AB + # was: AB goes in old and new section + # now: AB only in old section, but curcurve + # preseved. + l.append([curcurve,s]) + s = [A, B, C] + curcurve =maxcurve(cab, cbc, cabc) + else: + # Change of direction at B + l.append([curcurve,s]) + s = [B, C] + curcurve = cbc + + A = B + B = C + l.append([curcurve,s]) + + return l + + def remove_shorts(self, bbox): + # in self.list, if a point is close to the previous point, + # remove it. + if len(self.list) <= 2: + return + w = bbox.width()/10 + h = bbox.height()/10 + n = [self.list[0]] + leng = w*h*2*2 + for p in self.list[1:]: + l = p.sqlen(n[-1]) + if l > leng: + n.append(p) + self.list = n + + def text(self): + # OK, we have a list of points with curvature between. + # want to divide this into sections. + # for each 3 consectutive points ABC curve of ABC and AB and BC + # If all the same, they are all in a section. + # If not B starts a new section and the old ends on B or C... + BB = BBox(self.list[0]) + for p in self.list: + BB.add(p) + BB.finish() + self.bbox = BB + self.remove_shorts(BB) + sectlist = self.get_sectlist() + t = "" + for c, s in sectlist: + if c > 0: + dr = "R" # clockwise is to the Right + elif c < 0: + dr = "L" # counterclockwise to the Left + else: + dr = "S" # straight + bb = BBox(s[0]) + for p in s: + bb.add(p) + bb.finish() + # If all points are in some row or column, then + # line is S + rwdiff = False; cldiff = False + rw = bb.row(s[0]); cl=bb.col(s[0]) + for p in s: + if bb.row(p) != rw: rwdiff = True + if bb.col(p) != cl: cldiff = True + if not rwdiff or not cldiff: dr = "S" + + t1 = dr + t1 += "(%d)" % BB.relpos(bb) + t1 += "%d,%d" % (bb.box(s[0]), bb.box(s[-1])) + t += t1 + '.' + return t[:-1] + + + + + +class text_input: + def __init__(self, page, callout): + + self.page = page + self.callout = callout + self.colour = None + self.line = None + self.dict = Dictionary() + LoadDict(self.dict) + + page.connect("button_press_event", self.press) + page.connect("button_release_event", self.release) + page.connect("motion_notify_event", self.motion) + page.set_events(page.get_events() + | gtk.gdk.BUTTON_PRESS_MASK + | gtk.gdk.BUTTON_RELEASE_MASK + | gtk.gdk.POINTER_MOTION_MASK + | gtk.gdk.POINTER_MOTION_HINT_MASK) + + def set_colour(self, col): + self.colour = col + + def press(self, c, ev): + # Start a new line + self.line = [ [int(ev.x), int(ev.y)] ] + return + def release(self, c, ev): + if self.line == None: + return + if len(self.line) == 1: + self.callout('click', ev) + self.line = None + return + + sym = self.getsym() + if sym: + self.callout('sym', sym) + self.callout('redraw', None) + self.line = None + return + + def motion(self, c, ev): + if self.line: + if ev.is_hint: + x, y, state = ev.window.get_pointer() + else: + x = ev.x + y = ev.y + x = int(x) + y = int(y) + prev = self.line[-1] + if abs(prev[0] - x) < 10 and abs(prev[1] - y) < 10: + return + if self.colour: + c.window.draw_line(self.colour, prev[0],prev[1],x,y) + self.line.append([x,y]) + return + + def getsym(self): + alloc = self.page.get_allocation() + pagebb = BBox(Point(0,0)) + pagebb.add(Point(alloc.width, alloc.height)) + pagebb.finish(div = 2) + + p = PPath(self.line[1][0], self.line[1][1]) + for pp in self.line[1:]: + p.add(pp[0], pp[1]) + p.close() + patn = p.text() + pos = pagebb.relpos(p.bbox) + tpos = "mid" + if pos < 3: + tpos = "top" + if pos >= 6: + tpos = "bot" + sym = self.dict.match(patn, tpos) + if sym == None: + print "Failed to match pattern:", patn + return sym + + + + + +######################################################################## + + +def extend_array(ra, leng, val=None): + while len(ra) <= leng: + ra.append(val) + + +class Prod: + # A product that might be purchased + # These are stored in a list index by product number + def __init__(self, num, line): + # line is read from file, or string typed in for new + # product in which case it contains no comma. + # otherwise "Name,[R|I]{,Ln:m}" + self.num = num + words = line.split(',') + self.name = words[0] + self.regular = (len(words) > 1 and words[1] == 'R') + self.loc = [] + for loc in words[2:]: + if len(loc) == 0: + continue + n = loc[1:].split(':') + pl = int(n[0]) + lc = int(n[1]) + extend_array(self.loc, pl, -1) + self.loc[pl] = lc + + def format(self,f): + str = "I%d," % self.num + str += self.name + ',' + if self.regular: + str += 'R' + else: + str += 'I' + for i in range(len(self.loc)): + if self.loc[i] >= 0: + str += ",L%d:%d"%(i, self.loc[i]) + str += '\n' + f.write(str) + + +class Purch: + # A purchase that could be made + # A list of these is the current shopping list. + def __init__(self,source): + # source is a string read from a file, or + # a product being added to the list. + if source.__class__ == Prod: + self.prod = source.num + self.state = 'X' + self.comment = "" + elif source.__class__ == str: + l = source.split(',', 2) + self.prod = int(l[0]) + self.state = l[1] + self.comment = l[2] + else: + raise ValueError + + def format(self, f): + str = '%d,%s,%s\n' % (self.prod, self.state, self.comment) + f.write(str) + + def loc(self): + global place + p = products[self.prod] + if len(p.loc) <= place: + return -1 + if p.loc[place] == None: + return -1 + return p.loc[place] + + def locord(self): + global place + p = products[self.prod] + if len(p.loc) <= place: + return -1 + if p.loc[place] == -1 or p.loc[place] == None: + return -1 + return locorder[place].index(p.loc[place]) + +def purch_cmp(a,b): + pa = products[a.prod] + pb = products[b.prod] + la = a.locord() + lb = b.locord() + + if la < lb: + return -1 + if la > lb: + return 1 + # same location + return cmp(pa.name, pb.name) + + +def parse_places(l): + # P,n:name,... + w = l.split(',') + if w[0] != 'P': + return + for p in w[1:]: + w2 = p.split(':',1) + pos = int(w2[0]) + extend_array(places, pos,0) + places[pos] = w2[1] + +def parse_locations(l): + # Ln,m:loc,m2:loc2, + w = l.split(',') + if w[0][0] != 'L': + return + lnum = int(w[0][1:]) + loc = [] + order = [] + for l in w[1:]: + w2 = l.split(':',1) + pos = int(w2[0]) + extend_array(loc, pos) + loc[pos] = w2[1] + order.append(pos) + extend_array(locations, lnum) + extend_array(locorder, lnum) + locations[lnum] = loc + locorder[lnum] = order + +def parse_item(l): + # In,rest + w = l.split(',',1) + if w[0][0] != 'I': + return + lnum = int(w[0][1:]) + itm = Prod(lnum, w[1]) + extend_array(products, lnum) + products[lnum] = itm + +def load_table(f): + # read P L and I lines + l = f.readline() + while len(l) > 0: + l = l.strip() + if l[0] == 'P': + parse_places(l) + elif l[0] == 'L': + parse_locations(l) + elif l[0] == 'I': + parse_item(l) + l = f.readline() + +def save_table(name): + try: + f = open(name+".new", "w") + except: + return + f.write("P") + for i in range(len(places)): + f.write(",%d:%s" % (i, places[i])) + f.write("\n") + + for i in range(len(places)): + f.write("L%d" % i) + for j in locorder[i]: + f.write(",%d:%s" % (j, locations[i][j])) + f.write("\n") + for p in products: + if p: + p.format(f) + f.close() + os.rename(name+".new", name) + +table_timeout = None +def table_changed(): + global table_timeout + if table_timeout: + gobject.source_remove(table_timeout) + table_timeout = None + table_timeout = gobject.timeout_add(15*1000, table_tick) + +def table_tick(): + global table_timeout + if table_timeout: + gobject.source_remove(table_timeout) + table_timeout = None + save_table("Products") + +def load_list(f): + # Read item,state,comment from file to 'purch' list + l = f.readline() + while len(l) > 0: + l = l.strip() + purch.append(Purch(l)) + l = f.readline() + +def save_list(name): + try: + f = open(name+".new", "w") + except: + return + for p in purch: + if p.state != 'X': + p.format(f) + f.close() + os.rename(name+".new", name) + + +list_timeout = None +def list_changed(): + global list_timeout + if list_timeout: + gobject.source_remove(list_timeout) + list_timeout = None + list_timeout = gobject.timeout_add(15*1000, list_tick) + +def list_tick(): + global list_timeout + if list_timeout: + gobject.source_remove(list_timeout) + list_timeout = None + save_list("Purchases") + +def merge_list(purch, prod): + # add to purch any products not already there + have = [] + for p in purch: + extend_array(have, p.prod, False) + have[p.prod] = True + for p in prod: + if p and (p.num >= len(have) or not have[p.num]) : + purch.append(Purch(p)) + +def locname(purch): + if purch.loc() < 0: + return "Unknown" + else: + return locations[place][purch.loc()] + +class PurchView: + # A PurchView is the view on the list of possible purchases. + # We draw the names in a DrawingArea + # When a name is tapped, we call-out to possibly update it. + # We get a callback when: + # item state changes, so we need to change colour + # list (or sort-order) changes so complete redraw is needed + # zoom changes + # + + def __init__(self, zoom, callout, entry): + p = gtk.DrawingArea() + p.show() + self.widget = p + + fd = p.get_pango_context().get_font_description() + self.fd = fd + + self.callout = callout + self.zoom = 0 + self.set_zoom(zoom) + self.pixbuf = None + self.width = self.height = 0 + self.need_redraw = True + + self.colours = None + + self.plist = None + self.search = None + self.current = None + self.gonext = False + self.top = None + self.all_headers = False + + p.connect("expose-event", self.redraw) + p.connect("configure-event", self.reconfig) + + #p.connect("button_release_event", self.click) + p.set_events(gtk.gdk.EXPOSURE_MASK + | gtk.gdk.STRUCTURE_MASK) + + self.entry = entry + self.writing = text_input(p, self.stylus) + + def stylus(self, cmd, info): + if cmd == "click": + self.click(None, info) + return + if cmd == "redraw": + self.widget.queue_draw() + return + if cmd == "sym": + + if info == "": + self.entry.emit("backspace") + elif info == "": + self.entry.emit("activate") + else: + self.entry.emit("insert-at-cursor",info) + #print "Got Sym ", info + + def add_col(self, sym, col): + c = gtk.gdk.color_parse(col) + gc = self.widget.window.new_gc() + gc.set_foreground(self.widget.get_colormap().alloc_color(c)) + self.colours[sym] = gc + + def set_zoom(self, zoom): + if zoom > 50: + zoom = 50 + if zoom < 20: + zoom = 20 + if zoom == self.zoom: + return + self.need_redraw = True + self.zoom = zoom + s = pango.SCALE + for i in range(zoom): + s = s * 11 / 10 + self.fd.set_absolute_size(s) + self.widget.modify_font(self.fd) + met = self.widget.get_pango_context().get_metrics(self.fd) + + self.lineheight = (met.get_ascent() + met.get_descent()) / pango.SCALE + self.lineascent = met.get_ascent() / pango.SCALE + self.widget.queue_draw() + + def set_search(self, str): + self.search = str + self.need_redraw = True + self.widget.queue_draw() + + def reconfig(self, w, ev): + alloc = w.get_allocation() + if not self.pixbuf: + return + if alloc.width != self.width or alloc.height != self.height: + self.pixbuf = None + self.need_redraw = True + + def redraw(self, w, ev): + if self.colours == None: + self.colours = {} + self.add_col('N', "blue") # need + self.add_col('F', "darkgreen") # found + self.add_col('C', "red") # Cannot find + self.add_col('R', "orange")# Regular + self.add_col('X', "black") # No Need + self.add_col(' ', "white") # selected background + self.add_col('_', "black") # location separator + self.add_col('title', "cyan") + self.bg = self.widget.get_style().bg_gc[gtk.STATE_NORMAL] + self.writing.set_colour(self.colours['_']) + + + if self.need_redraw: + self.draw_buf() + + self.widget.window.draw_drawable(self.bg, self.pixbuf, 0, 0, 0, 0, + self.width, self.height) + + def draw_buf(self): + self.need_redraw = False + p = self.widget + if self.pixbuf == None: + alloc = p.get_allocation() + self.pixbuf = gtk.gdk.Pixmap(p.window, alloc.width, alloc.height) + self.width = alloc.width + self.height = alloc.height + self.pixbuf.draw_rectangle(self.bg, True, 0, 0, + self.width, self.height) + + if self.plist == None: + # Empty list, say so. + layout = self.widget.create_pango_layout("List Is Empty") + self.cols = 1 + (ink, log) = layout.get_pixel_extents() + (ex,ey,ew,eh) = log + self.pixbuf.draw_layout(self.colours['X'], (self.width-ew)/2, + (self.height-eh)/2, + layout) + return + + # find max width and height + maxw = 1; maxh = 1; longest = "nothing" + curr = None; top = 0 + visible = [] + curloc = None + for p in self.plist: + + if self.search == None and p.state == 'X': + # Don't normally show "noneed" entries + if p.prod == self.current and self.gonext: + curr = len(visible) + continue + if self.search != None and products[p.prod].name.lower().find(self.search.lower()) < 0: + # doesn't contain search string + continue + while p.loc() != curloc: + if not self.all_headers: + curloc = p.loc() + elif curloc == None: + curloc = -1 + else: + if curloc < 0: + i = -1 + else: + i = locorder[place].index(curloc) + + if i < len(locorder[place]) - 1: + curloc = locorder[place][i+1] + else: + break + if curloc < 0: + locstr = "Unknown" + elif curloc < len(locations[place]): + locstr = locations[place][curloc] + else: + locstr = None + + if locstr == None: + break + + if self.top == locstr: + top = len(visible) + visible.append(locstr) + + layout = self.widget.create_pango_layout(locstr) + (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents() + if ew > maxw: maxw = ew; longest = products[p.prod].name + if eh > maxh: maxh = eh + + if p.prod == self.top: + top = len(visible) + if curr != None and self.gonext and self.gonext == p.state: + self.gonext = False + self.current = p.prod + self.callout(p, 'auto') + if p.prod == self.current: + curr = len(visible) + visible.append(p) + layout = self.widget.create_pango_layout(products[p.prod].name) + (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents() + if ew > maxw: maxw = ew; longest = products[p.prod].name + if eh > maxh: maxh = eh + # print "mw=%d mh=%d lh=%d la=%d" % (maxw, maxh, self.lineheight, self.lineascent) + + if self.all_headers: + # any following headers with no items visible + while True: + if curloc == None: + curloc = -1 + else: + if curloc < 0: + i = -1 + else: + i = locorder[place].index(curloc) + + if i < len(locorder[place]) - 1: + curloc = locorder[place][i+1] + else: + break + if curloc < 0: + locstr = "Unknown" + elif curloc < len(locations[place]): + locstr = locations[place][curloc] + else: + locstr = None + + if locstr == None: + break + + if self.top == locstr: + top = len(visible) + visible.append(locstr) + + layout = self.widget.create_pango_layout(locstr) + (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents() + if ew > maxw: maxw = ew; longest = products[p.prod].name + if eh > maxh: maxh = eh + + self.gonext = False + truemaxw = maxw + maxw = int(maxw * 11/10) + if maxh > self.lineheight: + self.lineheight = maxh + # Find max rows/columns + rows = int(self.height / self.lineheight) + cols = int(self.width / maxw) + if rows < 1 or cols < 1: + if self.zoom > 10: + self.set_zoom(self.zoom - 1) + self.need_redraw = True + self.widget.queue_draw() + return + #print "rows=%d cols=%s" % (rows,cols) + colw = int(self.width / cols) + offset = (colw - truemaxw)/2 + self.offset = offset + + # check 'curr' is in appropriate range and + # possibly adjust 'top'. Then trim visible to top. + # Allow one blank line at the end. + cells = rows * cols + if cells >= len(visible): + # display everything + top = 0 + elif curr != None: + # make sure curr is in good range + if curr - top < rows/3: + top = curr - (cells - rows/3) + if top < 0: + top = 0 + if (cells - (curr - top)) < rows/3: + top = curr - rows / 3 + if len(visible) - top < cells-1: + top = len(visible) - (cells-1) + if top < 0: + top = 0 + else: + if len(visible) - top < cells-1: + top = len(visible) - (cells-1) + if top < 0: + top = 0 + + visible = visible[top:] + self.top = None + + self.visible = visible + self.rows = rows + self.cols = cols + + for r in range(rows): + for c in range(cols): + pos = c*rows+r + uline = False + if pos < len(visible): + if type(visible[pos]) == str: + strng = visible[pos] + state = 'title' + comment = False + if self.top == None: + self.top = visible[pos] + else: + strng = products[visible[pos].prod].name + uline = products[visible[pos].prod].regular + state = visible[pos].state + if self.top == None: + self.top = visible[pos].prod + comment = (not not visible[pos].comment) + else: + break + layout = self.widget.create_pango_layout(strng) + if uline: + a = pango.AttrList() + a.insert(pango.AttrUnderline(pango.UNDERLINE_SINGLE,0,len(strng))) + layout.set_attributes(a) + if curr != None and c*rows+r == curr - top: + self.pixbuf.draw_rectangle(self.colours[' '], True, + c*colw, r*self.lineheight, + colw, self.lineheight) + if state == 'title': + self.pixbuf.draw_rectangle(self.colours['_'], True, + c*colw+2, r*self.lineheight, + colw-4, self.lineheight) + self.pixbuf.draw_rectangle(self.colours['_'], False, + c*colw+2, r*self.lineheight, + colw-4-1, self.lineheight-1) + + if comment: + if offset > self.lineheight * 0.8: + w = int (self.lineheight * 0.8 * 0.9) + o = (offset - w) / 2 + else: + w = int(offset*0.9) + o = int((offset-w)/2) + vo = int((self.lineheight-w)/2) + self.pixbuf.draw_rectangle(self.colours[state], True, + c * colw + o, r * self.lineheight + vo, + w, w) + + + self.pixbuf.draw_layout(self.colours[state], + c * colw + offset, + r * self.lineheight, + layout) + + + def click(self, w, ev): + cw = self.width / self.cols + rh = self.lineheight + col = int(ev.x/cw) + row = int(ev.y/rh) + cell = col * self.rows + row + cellpos = ev.x - col * cw + pos = 0 + if cellpos < self.lineheight or cellpos < self.offset: + pos = -1 + elif cellpos > cw - self.lineheight: + pos = 1 + + if cell < len(self.visible) and type(self.visible[cell]) != str: + if pos == 0: + prod = self.visible[cell].prod + layout = self.widget.create_pango_layout(products[prod].name) + (ink, (ex,ey,ew,eh)) = layout.get_pixel_extents() + if cellpos > self.offset + ew: + pos = 1 + self.callout(self.visible[cell], pos) + if cell < len(self.visible) and type(self.visible[cell]) == str: + self.callout(self.visible[cell], 'heading') + + + def set_purch(self, purch): + if purch: + self.plist = purch + self.need_redraw = True + self.widget.queue_draw() + + def select(self, item = None, gonext = False): + if gonext: + self.gonext = gonext + elif item != None: + self.current = item + self.need_redraw = True + self.widget.queue_draw() + + def show_headers(self, all): + self.all_headers = all + self.need_redraw = True + self.widget.queue_draw() + +def swap_place(a,b): + places[a], places[b] = places[b], places[a] + locorder[a], locorder[b] = locorder[b], locorder[a] + locations[a], locations[b] = locations[b], locations[a] + for p in products: + if p: + extend_array(p.loc, a, -1) + extend_array(p.loc, b, -1) + p.loc[a], p.loc[b] = p.loc[b], p.loc[a] + table_changed() + +class ShoppingList: + + def __init__(self): + window = gtk.Window(gtk.WINDOW_TOPLEVEL) + window.connect("destroy", self.close_application) + window.set_title("Shopping List") + + self.purch = None + self.isize = gtk.icon_size_register("mine", 40, 40) + + # try to guess where user is so when he tried to change the + # location of something, we try 'here' first. + # Update this whenever we find something, or set a location. + # We clear to -1 when we change place + self.current_loc = -1 + + vb = gtk.VBox() + window.add(vb) + vb.show() + + top = gtk.HBox() + top.set_size_request(-1,40) + vb.pack_start(top, expand=False) + top.show() + + glob = gtk.HBox() + glob.set_size_request(-1,80) + glob.set_homogeneous(True) + vb.pack_end(glob, expand=False) + glob.show() + self.glob_control = glob + + locedit = gtk.HBox() + locedit.set_size_request(-1, 80) + vb.pack_end(locedit, expand=False) + locedit.hide() + self.locedit = locedit + + loc = gtk.HBox() + loc.set_size_request(-1,80) + vb.pack_end(loc, expand=False) + loc.hide() + self.loc = loc + + placeb = gtk.HBox() + placeb.set_size_request(-1,80) + vb.pack_end(placeb, expand=False) + placeb.hide() + self.place = placeb + + filemenu = gtk.HBox() + filemenu.set_homogeneous(True) + filemenu.set_size_request(-1,80) + vb.pack_end(filemenu, expand=False) + filemenu.hide() + self.filemenu = filemenu + + curr = gtk.HBox() + curr.set_size_request(-1,80) + curr.set_homogeneous(True) + vb.pack_end(curr, expand=False) + curr.show() + self.curr = curr + self.mode = 'curr' + + e = gtk.Entry() + + l = PurchView(34, self.item_selected, e) + vb.add(l.widget) + self.lview = l + + # multi use text-entry body + # used for search-string, comment-entry, item-entry/rename + # + ctx = e.get_pango_context() + fd = ctx.get_font_description() + fd.set_absolute_size(25*pango.SCALE) + e.modify_font(fd) + e.show() + top.add(e) + self.button(gtk.STOCK_OK, top, self.enter_change, expand = False) + self.entry = e + global XX + XX = e + self.entry_ignore = True + self.entry_mode = "comment" + e.connect("changed", self.entry_changed) + self.ecol_search = None + self.ecol_comment = None + self.ecol_item = None + self.ecol_loc = None + + # global control buttons + self.button(gtk.STOCK_REFRESH, glob, self.choose_place) + self.button(gtk.STOCK_PREFERENCES, glob, self.show_controls) + self.search_toggle = self.button(gtk.STOCK_FIND, glob, self.toggle_search, toggle=True) + self.button(gtk.STOCK_ADD, glob, self.add_item) + self.button(gtk.STOCK_EDIT, glob, self.change_name) + + # buttons to control current entry + self.button(gtk.STOCK_APPLY, curr, self.tick) + self.button(gtk.STOCK_CANCEL, curr, self.cross) + self.button(gtk.STOCK_JUMP_TO, curr, self.choose_loc) + self.button(gtk.STOCK_ZOOM_IN, curr, self.zoomin) + self.button(gtk.STOCK_ZOOM_OUT, curr, self.zoomout) + + # buttons for whole-list operations + self.button(gtk.STOCK_SAVE_AS, filemenu, self.record) + self.button(gtk.STOCK_HOME, filemenu, self.reset) + a = self.button(gtk.STOCK_CONVERT, filemenu, self.add_regulars) + self.add_reg_clicked = False + a.connect('leave',self.clear_regulars) + self.save_button = self.button(gtk.STOCK_SAVE, filemenu, self.save) + self.revert_button = self.button(gtk.STOCK_REVERT_TO_SAVED, filemenu, self.revert_to_saved) + self.revert_button.hide() + self.button(gtk.STOCK_QUIT, filemenu, self.close_application) + + # Buttons to change location of current entry + self.button(gtk.STOCK_GO_BACK, loc, self.prevloc, expand = False) + self.curr_loc = self.button("here", loc, self.curr_switch) + self.button(gtk.STOCK_GO_FORWARD, loc, self.nextloc, expand = False) + l = self.curr_loc.child + ctx = l.get_pango_context() + fd = ctx.get_font_description() + fd.set_absolute_size(25*pango.SCALE) + l.modify_font(fd) + + # buttons to edit the current location or place + self.button(gtk.STOCK_DELETE, locedit, self.locdelete) + self.button(gtk.STOCK_GO_UP, locedit, self.loc_move_up) + self.button(gtk.STOCK_GO_DOWN, locedit, self.loc_move_down) + self.button(gtk.STOCK_ADD, locedit, self.add_item) + self.button(gtk.STOCK_EDIT, locedit, self.change_name) + + + # Buttons to change current 'place' + self.button(gtk.STOCK_MEDIA_REWIND, placeb, self.prevplace, expand = False) + self.curr_place = self.button("HOME", placeb, self.curr_switch) + self.button(gtk.STOCK_MEDIA_FORWARD, placeb, self.nextplace, expand = False) + l = self.curr_place.child + ctx = l.get_pango_context() + fd = ctx.get_font_description() + fd.set_absolute_size(25*pango.SCALE) + l.modify_font(fd) + + + window.set_default_size(480, 640) + window.show() + + + def close_application(self, widget): + gtk.main_quit() + if table_timeout: + table_tick() + if list_timeout: + list_tick() + + def item_selected(self, purch, pos): + if pos == 'heading': + if self.mode != 'loc': + return + newloc = None + i = None + for i in range(len(locations[place])): + if locations[place][i] == purch: + newloc = i + if i == None: + return + extend_array(products[self.purch.prod].loc, place, -1) + products[self.purch.prod].loc[place] = newloc + table_changed() + self.lview.plist.sort(purch_cmp) + self.set_loc() + self.lview.select() + return + + if self.entry_mode == "comment": + self.lview.select(purch.prod) + self.purch = purch + self.entry_ignore = True + self.entry.set_text(purch.comment) + self.entry_ignore = False + self.set_loc() + #if pos < 0: + # self.tick(None) + #if pos > 0: + # self.cross(None) + if self.entry_mode == "search": + if pos != 'auto': + if purch.state == 'X' or purch.state == 'R': + purch.state = 'N' + elif purch.state == 'N': + purch.state = 'X' + self.lview.select(purch.prod) + self.purch = purch + self.set_loc() + list_changed() + + + def entry_changed(self, widget): + if self.entry_ignore: + return + if self.entry_mode == "search": + self.lview.set_search(self.entry.get_text()) + if self.entry_mode == "comment" and self.purch != None: + self.purch.comment = self.entry.get_text() + list_changed() + + def set_purch(self, purch): + self.lview.set_purch(purch) + + def button(self, name, bar, func, expand = True, toggle = False): + if toggle: + btn = gtk.ToggleButton() + else: + btn = gtk.Button() + if type(name) == str and name[0:3] != "gtk": + if not expand: + name = " " + name + " " + btn.set_label(name) + else: + img = gtk.image_new_from_stock(name, self.isize) + if not expand: + img.set_size_request(80, -1) + img.show() + btn.add(img) + btn.show() + bar.pack_start(btn, expand = expand) + btn.connect("clicked", func) + btn.set_focus_on_click(False) + return btn + + def record(self, widget): + # Record current purchase file in datestamped storage + save_list(time.strftime("purch-%Y%m%d-%H")) + + def reset(self, widget): + # Reset the shopping list. + # All regular to noneed + # if there were no regular, then + # found -> noneed + # cannot find -> need + found = False + for p in purch: + if p.state == 'R': + p.state = 'X' + found = True + if not found: + for p in purch: + if p.state == 'F': + p.state = 'X' + if p.state == 'C': + p.state = 'N' + list_changed() + self.lview.select() + + def add_regulars(self, widget): + if self.add_reg_clicked: + return self.all_regulars(widget) + # Mark all regulars (not already selected) as regulars + for p in purch: + if products[p.prod].regular and p.state == 'X': + p.state = 'R' + list_changed() + self.lview.select() + self.add_reg_clicked = True + + def all_regulars(self, widget): + # Mark all regulars and don'twant (not already selected) as regulars + for p in purch: + if p.state == 'X': + p.state = 'R' + list_changed() + self.lview.select() + def clear_regulars(self, widget): + self.add_reg_clicked = False + + def save(self, widget): + table_tick() + list_tick() + self.curr_switch(widget) + + + def setecol(self): + if self.ecol_search != None: + return + c = gtk.gdk.color_parse("yellow") + self.ecol_search = c + + c = gtk.gdk.color_parse("white") + self.ecol_comment = c + + c = gtk.gdk.color_parse("pink") + self.ecol_item = c + + c = gtk.gdk.color_parse('grey90') + self.ecol_loc = c + + def toggle_search(self, widget): + self.setecol() + if self.entry_mode == "item": + self.search_toggle.set_active(False) + return + if self.entry_mode == "search": + self.entry_ignore = True + self.entry.set_text(self.purch.comment) + self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_comment) + self.entry_mode = "comment" + self.lview.set_search(None) + self.entry_ignore = False + return + self.entry_mode = "search" + self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_search) + self.entry_ignore = True + self.entry.set_text("") + self.lview.set_search("") + self.entry_ignore = False + self.search_toggle.set_active(True) + + def choose_loc(self, widget): + # replace 'curr' buttons with 'loc' buttons + if self.purch == None: + return + self.curr.hide() + self.filemenu.hide() + self.loc.show() + self.locedit.show() + self.glob_control.hide() + self.mode = 'loc' + self.set_loc() + self.lview.show_headers(True) + + def set_loc(self): + loc = locname(self.purch) + self.current_loc = self.purch.loc() + self.curr_loc.child.set_text(places[place]+" / "+loc) + + def curr_switch(self, widget): + # set current item to current location, and switch back + self.lview.show_headers(False) + self.loc.hide() + self.locedit.hide() + self.glob_control.show() + self.place.hide() + self.filemenu.hide() + self.curr.show() + self.mode = 'curr' + + def show_controls(self, widget): + if self.mode == 'filemenu': + self.curr_switch(widget) + else: + self.lview.show_headers(False) + self.loc.hide() + self.place.hide() + self.curr.hide() + self.locedit.hide() + self.glob_control.show() + self.filemenu.show() + self.mode = 'filemenu' + + + def nextloc(self, widget): + if self.entry_mode != 'comment': + self.enter_change(None) + if self.current_loc != -1 and self.current_loc != self.purch.loc(): + newloc = self.current_loc + self.current_loc = -1 + elif self.purch.loc() < 0: + newloc = locorder[place][0] + else: + i = locorder[place].index(self.purch.loc()) + if i < len(locorder[place])-1: + newloc = locorder[place][i+1] + else: + return + + + if newloc < -1 or newloc >= len(locations[place]): + return + extend_array(products[self.purch.prod].loc, place, -1) + products[self.purch.prod].loc[place] = newloc + table_changed() + self.lview.plist.sort(purch_cmp) + self.set_loc() + self.lview.select() + + def prevloc(self, widget): + if self.entry_mode != 'comment': + self.enter_change(None) + if self.current_loc != -1 and self.current_loc != self.purch.loc(): + newloc = self.current_loc + self.current_loc = -1 + elif self.purch.loc() < 0: + return + else: + i = locorder[place].index(self.purch.loc()) + if i > 0: + newloc = locorder[place][i-1] + else: + newloc = -1 + + if newloc < -1: + return + extend_array(products[self.purch.prod].loc, place, -1) + products[self.purch.prod].loc[place] = newloc + table_changed() + self.lview.plist.sort(purch_cmp) + self.set_loc() + self.lview.select() + + def locdelete(self, widget): + # merge this location with the previous one + # So every product with this location needs to be changed, + # and the locorder updated. + if self.mode != "loc": + return + l = self.purch.loc() + if l < 0: + # cannot delete 'Unknown' + return + i = locorder[place].index(l) + if i == 0: + # nothing to merge with + return + self.backup_table() + newl = locorder[place][i-1] + for p in products: + if p != None: + if len(p.loc) > place: + if p.loc[place] == l: + p.loc[place] = newl + locorder[place][i:i+1] = [] + + table_changed() + self.lview.plist.sort(purch_cmp) + self.set_loc() + self.lview.select() + + def loc_move_up(self, widget): + global place + if self.mode == "loc": + l = self.purch.loc() + if l < 0: + # Cannot move 'unknown' + return + i = locorder[place].index(l) + if i == 0: + # nowhere to move + pass + else: + o = locorder[place][i-1:i+1] + locorder[place][i-1:i+1] = [o[1],o[0]] + table_changed() + self.lview.plist.sort(purch_cmp) + self.set_loc() + self.lview.select() + return + if self.mode == 'place': + if place > 0: + swap_place(place-1, place) + self.prevplace(None) + + def loc_move_down(self, widget): + global place + if self.mode == "loc": + l = self.purch.loc() + if l < 0: + # Cannot move 'unknown' + return + i = locorder[place].index(l) + if i+1 >= len(locorder[place]): + # nowhere to move + pass + else: + o = locorder[place][i:i+2] + locorder[place][i:i+2] = [o[1],o[0]] + table_changed() + self.lview.plist.sort(purch_cmp) + self.set_loc() + self.lview.select() + + if self.mode == 'place': + if place < len(places)-1: + swap_place(place, place+1) + self.nextplace(None) + + + def choose_place(self, widget): + if self.entry_mode != 'comment': + self.enter_change(None) + if self.mode == 'place': + self.curr_switch(widget) + return + self.pl_visible = True + self.lview.show_headers(False) + self.loc.hide() + self.locedit.show() + self.glob_control.hide() + self.curr.hide() + self.filemenu.hide() + self.mode = 'place' + self.place.show() + self.set_place() + + def set_place(self): + global place + if place >= len(places): + place = len(places) - 1 + if place < 0: + place = 0 + if place >= len(places): + pl = "Unknown Place" + else: + pl = places[place] + if place == 0: + prev = "" + else: + prev = places[place-1] + if place+1 >= len(places): + next = "" + else: + next = places[place+1] + spaces=" " + self.curr_place.child.set_markup( + ''+prev+'\n' + +spaces+pl+'\n' + +spaces+spaces+''+next+'') + self.current_loc = -1 + + def nextplace(self, widget): + global place + if place >= len(places)-1: + return + place += 1 + if self.lview.plist: + self.lview.plist.sort(purch_cmp) + self.set_place() + if self.purch: + self.lview.select() + else: + self.lview.set_purch(None) + + def prevplace(self, widget): + global place + if place <= 0: + return + place -= 1 + if self.lview.plist: + self.lview.plist.sort(purch_cmp) + self.set_place() + if self.purch: + self.lview.select() + else: + self.lview.set_purch(None) + + + def zoomin(self, widget): + self.lview.set_zoom(self.lview.zoom+1) + def zoomout(self, widget): + self.lview.set_zoom(self.lview.zoom-1) + + def tick(self, widget): + if self.purch != None: + if self.entry_mode == "search": + # set to regular + products[self.purch.prod].regular = True + #self.purch.state = 'R' + self.lview.select(gonext=self.purch.state) + list_changed(); table_changed() + return + oldstate = self.purch.state + if self.purch.state == 'N': self.purch.state = 'F' + elif self.purch.state == 'C': self.purch.state = 'F' + elif self.purch.state == 'R': self.purch.state = 'N' + elif self.purch.state == 'X': self.purch.state = 'N' + if self.purch.state == 'F': + self.current_loc = self.purch.loc() + self.lview.select(gonext = oldstate) + list_changed() + def cross(self, widget): + if self.purch != None: + oldstate = self.purch.state + if self.entry_mode == "search": + # set to regular + products[self.purch.prod].regular = False + if self.purch.state == 'R': + self.purch.state = 'X' + self.lview.select(gonext=oldstate) + list_changed(); table_changed() + return + + if self.purch.state == 'N': self.purch.state = 'C' + elif self.purch.state == 'F': self.purch.state = 'N' + elif self.purch.state == 'C': self.purch.state = 'X' + elif self.purch.state == 'R': self.purch.state = 'X' + elif self.purch.state == 'X': self.purch.state = 'X' + self.lview.select(gonext = oldstate) + list_changed() + + def add_item(self, widget): + global place + self.setecol() + if self.entry_mode == "search": + self.search_toggle.set_active(False) + if self.entry_mode != "comment": + return + if self.mode == 'curr': + self.entry_mode = "item" + self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_item) + self.entry_ignore = True + self.entry.set_text("") + self.purch = None + self.lview.select() + self.entry_ignore = False + elif self.mode == 'loc': + if self.purch == None: + return + if None in locations[place]: + lnum = locations[place].index(None) + else: + lnum = len(locations[place]) + locations[place].append(None) + locations[place][lnum] = 'NewLocation' + if self.purch.loc() == -1: + so = 0 + else: + so = locorder[place].index(self.purch.loc())+1 + locorder[place][so:so] = [lnum] + self.nextloc(None) + self.entry_mode = 'location' + self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_loc) + self.entry_ignore = True + self.entry.set_text('NewLocation') + self.entry.select_region(0,-1) + self.entry_ignore = False + elif self.mode == 'place': + if None in places: + pnum = places.index(None) + else: + pnum = len(places) + places.append(None) + places[pnum] = 'NewPlace' + extend_array(locations, pnum) + locations[pnum] = [] + extend_array(locorder, pnum) + locorder[pnum] = [] + place = pnum + self.lview.plist.sort(purch_cmp) + self.set_place() + self.entry_mode = 'place' + self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_loc) + self.entry_ignore = True + self.entry.set_text('NewPlace') + self.entry_ingore = False + def change_name(self, widget): + self.setecol() + if self.entry_mode == "search": + self.search_toggle.set_active(False) + if self.entry_mode == "item": + if self.purch != None: + self.entry.set_text(products[self.purch.prod].name) + return + if self.entry_mode == "location": + if self.purch != None: + self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_loc) + self.entry.set_text(locname(self.purch)) + return + if self.entry_mode == 'place': + self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_loc) + set.entry.set_text(places[place]) + return + if self.entry_mode != "comment": + return + if self.mode == 'curr': + if self.purch == None: + return + self.entry_mode = "item" + self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_item) + self.entry_ignore = True + self.entry.set_text(products[self.purch.prod].name) + self.entry_ignore = False + elif self.mode == 'loc': + if self.purch == None: + return + if self.purch.loc() < 0: + return + self.entry_mode = "location" + self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_loc) + self.entry_ignore = True + self.entry.set_text(locname(self.purch)) + self.entry_ignore = False + elif self.mode == 'place': + self.entry_mode = 'place' + self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_loc) + self.entry_ignore = True + self.entry.set_text(places[place]) + self.entry_inode = False + + # + # An item is being added or renamed. Commit the change + # If the new name is empty, that implys a delete. + # We only allow the delete if the state is 'X' and not regular + def update_item(self, name): + if len(name) > 0: + if self.purch == None: + # check for duplicates FIXME + num = len(products) + prod = Prod(num, name) + products.append(prod) + p = Purch(prod) + purch.append(p) + p.state = "N"; + self.purch = p + self.set_purch(purch) + self.lview.select(num) + self.lview.plist.sort(purch_cmp) + else: + products[self.purch.prod].name = name + self.lview.select() + self.lview.plist.sort(purch_cmp) + self.forget_backup() + table_changed() + list_changed() + elif self.purch: + # delete? + if self.purch.state == 'N': + # OK to delete + products[self.purch.prod] = None + try: + i = purch.index(self.purch) + except: + pass + else: + if i == 0: + new = -1 + else: + new = purch[i-1].prod + del purch[i] + self.lview.plist.sort(purch_cmp) + self.lview.select(new) + table_changed() + list_changed() + self.forget_backup() + + def update_location(self, name): + if len(name) > 0: + locations[place][self.purch.loc()] = name + self.set_loc() + self.lview.select() + table_changed() + return + # See if we can delete this location + # need to check all products that they aren't 'here' + for p in products: + if p and p.num != self.purch.prod and place < len(p.loc): + if p.loc[place] == self.purch.loc(): + return + # nothing here except 'purch' + l = self.purch.loc() + self.prevloc(None) + locations[place][l] = None + locorder[place].remove(l) + self.lview.plist.sort(purch_cmp) + self.lview.select() + table_changed() + list_changed() + + def update_place(self, name): + global place + if len(name) > 0: + places[place] = name + self.lview.select() + self.set_place() + table_changed() + return + if len(places) <= 1: + return + + self.backup_table() + places[place:place+1] = [] + locations[place:place+1] = [] + locorder[place:place+1] = [] + if place >= len(places): + place -= 1 + table_changed() + self.set_place() + self.lview.select() + + def backup_table(self): + save_table("Products.backup") + self.save_button.hide() + self.revert_button.show() + + def forget_backup(self): + self.save_button.show() + self.revert_button.hide() + + def revert_to_saved(self, widget): + try: + f = open("Products.backup") + products = [] + locations = [] + locorder = [] + places = [] + load_table(f) + f.close() + except: + pass + + self.forget_backup() + + + def enter_change(self, widget): + name = self.entry.get_text() + mode = self.entry_mode + # This is need to avoid recursion as update_* calls {next,prev}loc + self.entry_mode = 'comment' + if mode == 'item': + self.update_item(name) + elif mode == 'location': + self.update_location(name) + elif mode == 'place': + self.update_place(name) + + self.entry_mode = "comment" + self.entry.modify_base(gtk.STATE_NORMAL, self.ecol_comment) + self.entry_ignore = True + self.entry.set_text("") + self.entry_ignore = False + +def main(): + gtk.main() + return 0 + +if __name__ == "__main__": + + home = os.getenv("HOME") + #p = os.path.join(home, "shopping") + p = "/data/shopping" + try: + os.mkdir(p) + except: + pass + if os.path.exists(p): + os.chdir(p) + else: + os.chdir(home) + + products = [] + locations = [] + locorder = [] + places = [] + try: + f = open("Products") + load_table(f) + f.close() + except: + places = ['Home'] + locorder = [[]] + locations =[[]] + + purch = [] + try: + f = open("Purchases") + except: + pass + else: + load_list(f) + f.close() + merge_list(purch, products) + + + place = 0 + purch.sort(purch_cmp) + + sl = ShoppingList() + sl.set_purch(purch) + ss = gtk.settings_get_default() + ss.set_long_property("gtk-cursor-blink", 0, "shop") + main() diff --git a/sms/exesms b/sms/exesms new file mode 100644 index 0000000..7929411 --- /dev/null +++ b/sms/exesms @@ -0,0 +1,48 @@ +#!/usr/bin/env python + +import urllib, sys, os + +def make_url(sender,recipient,mesg): + if recipient[0] == '+': + recipient = recipient[1:] + elif recipient[0:2] != '04': + print "Invalid SMS address: " + recipient + sys.exit(1) + + return "https://www.exetel.com.au/sendsms/api_sms.php?username=0293169905&password=birtwhistle&mobilenumber=%s&message=%s&sender=%s&messagetype=Text" % ( + recipient, + urllib.quote(mesg), + sender + ) + + +def send(sender, recipient, mesg): + try: + f = urllib.urlopen(make_url(sender,recipient, mesg)) + except: + rv = 2 + print "Cannot connect: " + sys.exc_value.strerror + else: + rv = 0 + for l in f: + l = l.strip() + if not l: + continue + f = l.split('|') + if len(f) == 5: + if f[0] != '1': + rv = 1 + m = f[4] + if m[-4:] == '
': + m = m[0:-4] + print m, + else: + rv = 1 + print l, + print + return rv + +os.system("/bin/sh /root/pppon") +ec = send(sys.argv[1], sys.argv[2], sys.argv[3]) +sys.exit(ec) + diff --git a/sms/sendsms.py b/sms/sendsms.py new file mode 100755 index 0000000..ad9de15 --- /dev/null +++ b/sms/sendsms.py @@ -0,0 +1,1614 @@ +#!/usr/bin/env python + +# Create/edit/send/display/search SMS messages. +# Two main displays: Create and display +# Create: +# Allow entry of recipient and text of SMS message and allow basic editting +# When entering recipient, text box can show address matches for selection +# Bottom buttons are "Select".. +# When entering text, if there is no text, buttom buttons are: +# "Browse", "Close" +# If these is some text, bottom buttons are: +# "Send", Save" +# +# Display: +# We usually display a list of messages which can be selected from +# There is a 'search' box to restrict message to those with a string +# Options for selected message are: +# Delete Reply View Open(for draft)/Forward(for non-draft) +# In View mode, the whole text is displayed and the 'View' button becomes "Index" +# or "Show List" or "ReadIt" +# General options are: +# New Config List +# New goes to Edit +# +# Delete becomes Undelete and can undelete a whole stack. +# Delete can become undelete without deleting be press-and-hold +# +# +# Messages are sent using a separate program. e.g. sms-gsm +# Different recipients can use different programs based on flag in address book. +# Somehow senders can be configured. +# e.g. sms-exetel needs username, password, sender strings. +# press-and-hold on the send button allows a sender to be selected. +# +# +# Send an SMS message using some backend. + +# +# +# TODO: +# 'del' to return to 'list' view +# top buttons: del, view/list, new/open/reply +# so can only reply when viewing whole message +# Bottom: +# all: sent recv +# send: all draft +# recv: all new +# draft: all sent +# new: all recv +# DONE handle newline chars in summary +# DONE cope properly when the month changes. +# switch-to-'new' on 'expose' +# 'draft' button becomes 'cancel' when all is empty +# DONE better display of name/number of destination +# jump to list mode when change 'list' +# 'open' becomes 'reply' when current message was received. +# new message becomes non-new when replied to +# '' button doesn't select, but just makes choice. +# 'new' becomes 'select' when has been pressed. +# DONE Start in 'read', preferrably 'new' +# DONE always report status from send +# DONE draft/new/recv/sent/all - 5 groups +# DONE allow scrolling through list +# DONE + prefix to work +# DONE compose as 'GSM' or 'EXE' send +# DONE somehow do addressbook lookup for compose +# DONE addressbook lookup for display +# On 'send' move to 'sent' (not draft) and display list +# When open 'draft', delete from drafts... or later.. +# When 'reply' to new message, make it not 'new' +# +# get 'cut' to work from phone number entry. +# how to configure sender... +# need to select 'number only' mode for entry +# need drop-down of common numbers +# DONE text wrapping +# punctuation +# faster text input!!! +# DONE status message of transmission +# DONE maybe go to 'past messages' on send - need to go somewhere +# cut from other sources?? +# DONE scroll if message is too long! +# +# DONE reread sms file when changing view +# Don't add drafts that have not been changed... or +# When opening a draft, delete it... or replace when re-add +# DONE when sending a message, store - as draft if send failed +# DONE show the 'send' status somewhere +# DONE add a 'new' button from 'list' to 'send' +# Need 'reply' button.. Make 'open' show 'reply' when 'to' me. +# Scroll when near top or bottom +# hide status line when not needed. +# searching mesg list +# 'folder' view - by month or day +# highlight 'new' and 'draft' messages in different colour +# support 'sent' and 'received' distinction +# when return from viewing a 'new' message, clear the 'new' status +# enable starting in 'listing/New' mode + +import gtk, pango +import sys, time, os, re +import struct +from subprocess import Popen, PIPE +from storesms import SMSstore, SMSmesg +import dnotify + +########################################################### +# Writing recognistion code +import math + + +def LoadDict(dict): + # Upper case. + # Where they are like lowercase, we either double + # the last stroke (L, J, I) or draw backwards (S, Z, X) + # U V are a special case + + dict.add('A', "R(4)6,8") + dict.add('B', "R(4)6,4.R(7)1,6") + dict.add('B', "R(4)6,4.L(4)2,8.R(7)1,6") + dict.add('B', "S(6)7,1.R(4)6,4.R(7)0,6") + dict.add('C', "R(4)8,2") + dict.add('D', "R(4)6,6") + dict.add('E', "L(1)2,8.L(7)2,8") + # double the stem for F + dict.add('F', "L(4)2,6.S(3)7,1") + dict.add('F', "S(1)5,3.S(3)1,7.S(3)7,1") + + dict.add('G', "L(4)2,5.S(8)1,7") + dict.add('G', "L(4)2,5.R(8)6,8") + # FIXME I need better straight-curve alignment + dict.add('H', "S(3)1,7.R(7)6,8.S(5)7,1") + dict.add('H', "L(3)0,5.R(7)6,8.S(5)7,1") + # capital I is down/up + dict.add('I', "S(4)1,7.S(4)7,1") + + # Capital J has a left/right tail + dict.add('J', "R(4)1,6.S(7)3,5") + + dict.add('K', "L(4)0,2.R(4)6,6.L(4)2,8") + + # Capital L, like J, doubles the foot + dict.add('L', "L(4)0,8.S(7)4,3") + + dict.add('M', "R(3)6,5.R(5)3,8") + dict.add('M', "R(3)6,5.L(1)0,2.R(5)3,8") + + dict.add('N', "R(3)6,8.L(5)0,2") + + # Capital O is CW, but can be CCW in special dict + dict.add('O', "R(4)1,1", bot='0') + + dict.add('P', "R(4)6,3") + dict.add('Q', "R(4)7,7.S(8)0,8") + + dict.add('R', "R(4)6,4.S(8)0,8") + + # S is drawn bottom to top. + dict.add('S', "L(7)6,1.R(1)7,2") + + # Double the stem for capital T + dict.add('T', "R(4)0,8.S(5)7,1") + + # U is L to R, V is R to L for now + dict.add('U', "L(4)0,2") + dict.add('V', "R(4)2,0") + + dict.add('W', "R(5)2,3.L(7)8,6.R(3)5,0") + dict.add('W', "R(5)2,3.R(3)5,0") + + dict.add('X', "R(4)6,0") + + dict.add('Y',"L(1)0,2.R(5)4,6.S(5)6,2") + dict.add('Y',"L(1)0,2.S(5)2,7.S(5)7,2") + + dict.add('Z', "R(4)8,2.L(4)6,0") + + # Lower case + dict.add('a', "L(4)2,2.L(5)1,7") + dict.add('a', "L(4)2,2.L(5)0,8") + dict.add('a', "L(4)2,2.S(5)0,8") + dict.add('b', "S(3)1,7.R(7)6,3") + dict.add('c', "L(4)2,8", top='C') + dict.add('d', "L(4)5,2.S(5)1,7") + dict.add('d', "L(4)5,2.L(5)0,8") + dict.add('e', "S(4)3,5.L(4)5,8") + dict.add('e', "L(4)3,8") + dict.add('f', "L(4)2,6", top='F') + dict.add('f', "S(1)5,3.S(3)1,7", top='F') + dict.add('g', "L(1)2,2.R(4)1,6") + dict.add('h', "S(3)1,7.R(7)6,8") + dict.add('h', "L(3)0,5.R(7)6,8") + dict.add('i', "S(4)1,7", top='I', bot='1') + dict.add('j', "R(4)1,6", top='J') + dict.add('k', "L(3)0,5.L(7)2,8") + dict.add('k', "L(4)0,5.R(7)6,6.L(7)1,8") + dict.add('l', "L(4)0,8", top='L') + dict.add('l', "S(3)1,7.S(7)3,5", top='L') + dict.add('m', "S(3)1,7.R(3)6,8.R(5)6,8") + dict.add('m', "L(3)0,2.R(3)6,8.R(5)6,8") + dict.add('n', "S(3)1,7.R(4)6,8") + dict.add('o', "L(4)1,1", top='O', bot='0') + dict.add('p', "S(3)1,7.R(4)6,3") + dict.add('q', "L(1)2,2.L(5)1,5") + dict.add('q', "L(1)2,2.S(5)1,7.R(8)6,2") + dict.add('q', "L(1)2,2.S(5)1,7.S(5)1,7") + # FIXME this double 1,7 is due to a gentle where the + # second looks like a line because it is narrow.?? + dict.add('r', "S(3)1,7.R(4)6,2") + dict.add('s', "L(1)2,7.R(7)1,6", top='S', bot='5') + dict.add('t', "R(4)0,8", top='T', bot='7') + dict.add('t', "S(1)3,5.S(5)1,7", top='T', bot='7') + dict.add('u', "L(4)0,2.S(5)1,7") + dict.add('v', "L(4)0,2.L(2)0,2") + dict.add('w', "L(3)0,2.L(5)0,2", top='W') + dict.add('w', "L(3)0,5.R(7)6,8.L(5)3,2", top='W') + dict.add('w', "L(3)0,5.L(5)3,2", top='W') + dict.add('x', "L(4)0,6", top='X') + dict.add('y', "L(1)0,2.R(5)4,6", top='Y') # if curved + dict.add('y', "L(1)0,2.S(5)2,7", top='Y') + dict.add('z', "R(4)0,6.L(4)2,8", top='Z', bot='2') + + # Digits + dict.add('0', "L(4)7,7") + dict.add('0', "R(4)7,7") + dict.add('1', "S(4)7,1") + dict.add('2', "R(4)0,6.S(7)3,5") + dict.add('2', "R(4)3,6.L(4)2,8") + dict.add('3', "R(1)0,6.R(7)1,6") + dict.add('4', "L(4)7,5") + dict.add('5', "L(1)2,6.R(7)0,3") + dict.add('5', "L(1)2,6.L(4)0,8.R(7)0,3") + dict.add('6', "L(4)2,3") + dict.add('7', "S(1)3,5.R(4)1,6") + dict.add('7', "R(4)0,6") + dict.add('7', "R(4)0,7") + dict.add('8', "L(4)2,8.R(4)4,2.L(3)6,1") + dict.add('8', "L(1)2,8.R(7)2,0.L(1)6,1") + dict.add('8', "L(0)2,6.R(7)0,1.L(2)6,0") + dict.add('8', "R(4)2,6.L(4)4,2.R(5)8,1") + dict.add('9', "L(1)2,2.S(5)1,7") + + dict.add(' ', "S(4)3,5") + dict.add('', "S(4)5,3") + dict.add('-', "S(4)3,5.S(4)5,3") + dict.add('_', "S(4)3,5.S(4)5,3.S(4)3,5") + dict.add("", "S(4)5,3.S(3)3,5") + dict.add("","S(4)3,5.S(5)5,3") + dict.add("", "S(4)7,1.S(1)1,7") # "" + dict.add("","S(4)1,7.S(7)7,1") # "" + dict.add("", "S(4)2,6") + + +class DictSegment: + # Each segment has for elements: + # direction: Right Straight Left (R=cw, L=ccw) + # location: 0-8. + # start: 0-8 + # finish: 0-8 + # Segments match if there difference at each element + # is 0, 1, or 3 (RSL coded as 012) + # A difference of 1 required both to be same / 3 + # On a match, return number of 0s + # On non-match, return -1 + def __init__(self, str): + # D(L)S,R + # 0123456 + self.e = [0,0,0,0] + if len(str) != 7: + raise ValueError + if str[1] != '(' or str[3] != ')' or str[5] != ',': + raise ValueError + if str[0] == 'R': + self.e[0] = 0 + elif str[0] == 'L': + self.e[0] = 2 + elif str[0] == 'S': + self.e[0] = 1 + else: + raise ValueError + + self.e[1] = int(str[2]) + self.e[2] = int(str[4]) + self.e[3] = int(str[6]) + + def match(self, other): + cnt = 0 + for i in range(0,4): + diff = abs(self.e[i] - other.e[i]) + if diff == 0: + cnt += 1 + elif diff == 3: + pass + elif diff == 1 and (self.e[i]/3 == other.e[i]/3): + pass + else: + return -1 + return cnt + +class DictPattern: + # A Dict Pattern is a list of segments. + # A parsed pattern matches a dict pattern if + # the are the same nubmer of segments and they + # all match. The value of the match is the sum + # of the individual matches. + # A DictPattern is printers as segments joined by periods. + # + def __init__(self, str): + self.segs = map(DictSegment, str.split(".")) + def match(self,other): + if len(self.segs) != len(other.segs): + return -1 + cnt = 0 + for i in range(0,len(self.segs)): + m = self.segs[i].match(other.segs[i]) + if m < 0: + return m + cnt += m + return cnt + + +class Dictionary: + # The dictionary hold all the pattern for symbols and + # performs lookup + # Each pattern in the directionary can be associated + # with 3 symbols. One when drawing in middle of screen, + # one for top of screen, one for bottom. + # Often these will all be the same. + # This allows e.g. s and S to have the same pattern in different + # location on the touchscreen. + # A match requires a unique entry with a match that is better + # than any other entry. + # + def __init__(self): + self.dict = [] + def add(self, sym, pat, top = None, bot = None): + if top == None: top = sym + if bot == None: bot = sym + self.dict.append((DictPattern(pat), sym, top, bot)) + + def _match(self, p): + max = -1 + val = None + for (ptn, sym, top, bot) in self.dict: + cnt = ptn.match(p) + if cnt > max: + max = cnt + val = (sym, top, bot) + elif cnt == max: + val = None + return val + + def match(self, str, pos = "mid"): + p = DictPattern(str) + m = self._match(p) + if m == None: + return m + (mid, top, bot) = self._match(p) + if pos == "top": return top + if pos == "bot": return bot + return mid + + +class Point: + # This represents a point in the path and all the points leading + # up to it. It allows us to find the direction and curvature from + # one point to another + # We store x,y, and sum/cnt of points so far + def __init__(self,x,y) : + self.xsum = x + self.ysum = y + self.x = x + self.y = y + self.cnt = 1 + + def copy(self): + n = Point(0,0) + n.xsum = self.xsum + n.ysum = self.ysum + n.x = self.x + n.y = self.y + n.cnt = self.cnt + return n + + def add(self,x,y): + if self.x == x and self.y == y: + return + self.x = x + self.y = y + self.xsum += x + self.ysum += y + self.cnt += 1 + + def xlen(self,p): + return abs(self.x - p.x) + def ylen(self,p): + return abs(self.y - p.y) + def sqlen(self,p): + x = self.x - p.x + y = self.y - p.y + return x*x + y*y + + def xdir(self,p): + if self.x > p.x: + return 1 + if self.x < p.x: + return -1 + return 0 + def ydir(self,p): + if self.y > p.y: + return 1 + if self.y < p.y: + return -1 + return 0 + def curve(self,p): + if self.cnt == p.cnt: + return 0 + x1 = p.x ; y1 = p.y + (x2,y2) = self.meanpoint(p) + x3 = self.x; y3 = self.y + + curve = (y3-y1)*(x2-x1) - (y2-y1)*(x3-x1) + curve = curve * 100 / ((y3-y1)*(y3-y1) + + (x3-x1)*(x3-x1)) + if curve > 6: + return 1 + if curve < -6: + return -1 + return 0 + + def Vcurve(self,p): + if self.cnt == p.cnt: + return 0 + x1 = p.x ; y1 = p.y + (x2,y2) = self.meanpoint(p) + x3 = self.x; y3 = self.y + + curve = (y3-y1)*(x2-x1) - (y2-y1)*(x3-x1) + curve = curve * 100 / ((y3-y1)*(y3-y1) + + (x3-x1)*(x3-x1)) + return curve + + def meanpoint(self,p): + x = (self.xsum - p.xsum) / (self.cnt - p.cnt) + y = (self.ysum - p.ysum) / (self.cnt - p.cnt) + return (x,y) + + def is_sharp(self,A,C): + # Measure the cosine at self between A and C + # as A and C could be curve, we take the mean point on + # self.A and self.C as the points to find cosine between + (ax,ay) = self.meanpoint(A) + (cx,cy) = self.meanpoint(C) + a = ax-self.x; b=ay-self.y + c = cx-self.x; d=cy-self.y + x = a*c + b*d + y = a*d - b*c + h = math.sqrt(x*x+y*y) + if h > 0: + cs = x*1000/h + else: + cs = 0 + return (cs > 900) + +class BBox: + # a BBox records min/max x/y of some Points and + # can subsequently report row, column, pos of each point + # can also locate one bbox in another + + def __init__(self, p): + self.minx = p.x + self.maxx = p.x + self.miny = p.y + self.maxy = p.y + + def width(self): + return self.maxx - self.minx + def height(self): + return self.maxy - self.miny + + def add(self, p): + if p.x > self.maxx: + self.maxx = p.x + if p.x < self.minx: + self.minx = p.x + + if p.y > self.maxy: + self.maxy = p.y + if p.y < self.miny: + self.miny = p.y + def finish(self, div = 3): + # if aspect ratio is bad, we adjust max/min accordingly + # before setting [xy][12]. We don't change self.min/max + # as they are used to place stroke in bigger bbox. + # Normally divisions are at 1/3 and 2/3. They can be moved + # by setting div e.g. 2 = 1/2 and 1/2 + (minx,miny,maxx,maxy) = (self.minx,self.miny,self.maxx,self.maxy) + if (maxx - minx) * 3 < (maxy - miny) * 2: + # too narrow + mid = int((maxx + minx)/2) + halfwidth = int ((maxy - miny)/3) + minx = mid - halfwidth + maxx = mid + halfwidth + if (maxy - miny) * 3 < (maxx - minx) * 2: + # too wide + mid = int((maxy + miny)/2) + halfheight = int ((maxx - minx)/3) + miny = mid - halfheight + maxy = mid + halfheight + + div1 = div - 1 + self.x1 = int((div1*minx + maxx)/div) + self.x2 = int((minx + div1*maxx)/div) + self.y1 = int((div1*miny + maxy)/div) + self.y2 = int((miny + div1*maxy)/div) + + def row(self, p): + # 0, 1, 2 - top to bottom + if p.y <= self.y1: + return 0 + if p.y < self.y2: + return 1 + return 2 + def col(self, p): + if p.x <= self.x1: + return 0 + if p.x < self.x2: + return 1 + return 2 + def box(self, p): + # 0 to 9 + return self.row(p) * 3 + self.col(p) + + def relpos(self,b): + # b is a box within self. find location 0-8 + if b.maxx < self.x2 and b.minx < self.x1: + x = 0 + elif b.minx > self.x1 and b.maxx > self.x2: + x = 2 + else: + x = 1 + if b.maxy < self.y2 and b.miny < self.y1: + y = 0 + elif b.miny > self.y1 and b.maxy > self.y2: + y = 2 + else: + y = 1 + return y*3 + x + + +def different(*args): + cur = 0 + for i in args: + if cur != 0 and i != 0 and cur != i: + return True + if cur == 0: + cur = i + return False + +def maxcurve(*args): + for i in args: + if i != 0: + return i + return 0 + +class PPath: + # a PPath refines a list of x,y points into a list of Points + # The Points mark out segments which end at significant Points + # such as inflections and reversals. + + def __init__(self, x,y): + + self.start = Point(x,y) + self.mid = Point(x,y) + self.curr = Point(x,y) + self.list = [ self.start ] + + def add(self, x, y): + self.curr.add(x,y) + + if ( (abs(self.mid.xdir(self.start) - self.curr.xdir(self.mid)) == 2) or + (abs(self.mid.ydir(self.start) - self.curr.ydir(self.mid)) == 2) or + (abs(self.curr.Vcurve(self.start))+2 < abs(self.mid.Vcurve(self.start)))): + pass + else: + self.mid = self.curr.copy() + + if self.curr.xlen(self.mid) > 4 or self.curr.ylen(self.mid) > 4: + self.start = self.mid.copy() + self.list.append(self.start) + self.mid = self.curr.copy() + + def close(self): + self.list.append(self.curr) + + def get_sectlist(self): + if len(self.list) <= 2: + return [[0,self.list]] + l = [] + A = self.list[0] + B = self.list[1] + s = [A,B] + curcurve = B.curve(A) + for C in self.list[2:]: + cabc = C.curve(A) + cab = B.curve(A) + cbc = C.curve(B) + if B.is_sharp(A,C) and not different(cabc, cab, cbc, curcurve): + # B is too pointy, must break here + l.append([curcurve, s]) + s = [B, C] + curcurve = cbc + elif not different(cabc, cab, cbc, curcurve): + # all happy + s.append(C) + if curcurve == 0: + curcurve = maxcurve(cab, cbc, cabc) + elif not different(cabc, cab, cbc) : + # gentle inflection along AB + # was: AB goes in old and new section + # now: AB only in old section, but curcurve + # preseved. + l.append([curcurve,s]) + s = [A, B, C] + curcurve =maxcurve(cab, cbc, cabc) + else: + # Change of direction at B + l.append([curcurve,s]) + s = [B, C] + curcurve = cbc + + A = B + B = C + l.append([curcurve,s]) + + return l + + def remove_shorts(self, bbox): + # in self.list, if a point is close to the previous point, + # remove it. + if len(self.list) <= 2: + return + w = bbox.width()/10 + h = bbox.height()/10 + n = [self.list[0]] + leng = w*h*2*2 + for p in self.list[1:]: + l = p.sqlen(n[-1]) + if l > leng: + n.append(p) + self.list = n + + def text(self): + # OK, we have a list of points with curvature between. + # want to divide this into sections. + # for each 3 consectutive points ABC curve of ABC and AB and BC + # If all the same, they are all in a section. + # If not B starts a new section and the old ends on B or C... + BB = BBox(self.list[0]) + for p in self.list: + BB.add(p) + BB.finish() + self.bbox = BB + self.remove_shorts(BB) + sectlist = self.get_sectlist() + t = "" + for c, s in sectlist: + if c > 0: + dr = "R" # clockwise is to the Right + elif c < 0: + dr = "L" # counterclockwise to the Left + else: + dr = "S" # straight + bb = BBox(s[0]) + for p in s: + bb.add(p) + bb.finish() + # If all points are in some row or column, then + # line is S + rwdiff = False; cldiff = False + rw = bb.row(s[0]); cl=bb.col(s[0]) + for p in s: + if bb.row(p) != rw: rwdiff = True + if bb.col(p) != cl: cldiff = True + if not rwdiff or not cldiff: dr = "S" + + t1 = dr + t1 += "(%d)" % BB.relpos(bb) + t1 += "%d,%d" % (bb.box(s[0]), bb.box(s[-1])) + t += t1 + '.' + return t[:-1] + + +class text_input: + def __init__(self, page, callout): + + self.page = page + self.callout = callout + self.colour = None + self.line = None + self.dict = Dictionary() + self.active = True + LoadDict(self.dict) + + page.connect("button_press_event", self.press) + page.connect("button_release_event", self.release) + page.connect("motion_notify_event", self.motion) + page.set_events(page.get_events() + | gtk.gdk.BUTTON_PRESS_MASK + | gtk.gdk.BUTTON_RELEASE_MASK + | gtk.gdk.POINTER_MOTION_MASK + | gtk.gdk.POINTER_MOTION_HINT_MASK) + + def set_colour(self, col): + self.colour = col + + def press(self, c, ev): + if not self.active: + return + # Start a new line + self.line = [ [int(ev.x), int(ev.y)] ] + if not ev.send_event: + self.page.stop_emission('button_press_event') + return + def release(self, c, ev): + if self.line == None: + return + if len(self.line) == 1: + self.callout('click', ev) + self.line = None + return + + sym = self.getsym() + if sym: + self.callout('sym', sym) + self.callout('redraw', None) + self.line = None + return + + def motion(self, c, ev): + if self.line: + if ev.is_hint: + x, y, state = ev.window.get_pointer() + else: + x = ev.x + y = ev.y + x = int(x) + y = int(y) + prev = self.line[-1] + if abs(prev[0] - x) < 10 and abs(prev[1] - y) < 10: + return + if self.colour: + c.window.draw_line(self.colour, prev[0],prev[1],x,y) + self.line.append([x,y]) + return + + def getsym(self): + alloc = self.page.get_allocation() + pagebb = BBox(Point(0,0)) + pagebb.add(Point(alloc.width, alloc.height)) + pagebb.finish(div = 2) + + p = PPath(self.line[1][0], self.line[1][1]) + for pp in self.line[1:]: + p.add(pp[0], pp[1]) + p.close() + patn = p.text() + pos = pagebb.relpos(p.bbox) + tpos = "mid" + if pos < 3: + tpos = "top" + if pos >= 6: + tpos = "bot" + sym = self.dict.match(patn, tpos) + if sym == None: + print "Failed to match pattern:", patn + return sym + + + + + +######################################################################## + + + +class FingerText(gtk.TextView): + def __init__(self): + gtk.TextView.__init__(self) + self.set_wrap_mode(gtk.WRAP_WORD_CHAR) + self.exphan = self.connect('expose-event', self.config) + self.input = text_input(self, self.stylus) + + def config(self, *a): + self.disconnect(self.exphan) + c = gtk.gdk.color_parse('red') + gc = self.window.new_gc() + gc.set_foreground(self.get_colormap().alloc_color(c)) + #gc.set_line_attributes(2, gtk.gdk.LINE_SOLID, gtk.gdk.CAP_ROUND, gtk.gdk.JOIN_ROUND) + gc.set_subwindow(gtk.gdk.INCLUDE_INFERIORS) + self.input.set_colour(gc) + + def stylus(self, cmd, info): + if cmd == "sym": + tl = self.get_toplevel() + w = tl.get_focus() + if w == None: + w = self + ev = gtk.gdk.Event(gtk.gdk.KEY_PRESS) + ev.window = w.window + if info == '': + ev.keyval = 65288 + ev.hardware_keycode = 22 + else: + (ev.keyval,) = struct.unpack_from("b", info) + w.emit('key_press_event', ev) + #self.get_buffer().insert_at_cursor(info) + if cmd == 'click': + self.grab_focus() + if not info.send_event: + info.send_event = True + ev2 = gtk.gdk.Event(gtk.gdk.BUTTON_PRESS) + ev2.send_event = True + ev2.window = info.window + ev2.time = info.time + ev2.x = info.x + ev2.y = info.y + ev2.button = info.button + self.emit('button_press_event', ev2) + self.emit('button_release_event', info) + if cmd == 'redraw': + self.queue_draw() + + def insert_at_cursor(self, text): + self.get_buffer().insert_at_cursor(text) + +class FingerEntry(gtk.Entry): + def __init__(self): + gtk.Entry.__init__(self) + + def insert_at_cursor(self, text): + c = self.get_property('cursor-position') + t = self.get_text() + t = t[0:c]+text+t[c:] + self.set_text(t) + +class SMSlist(gtk.DrawingArea): + def __init__(self, getlist): + gtk.DrawingArea.__init__(self) + self.pixbuf = None + self.width = self.height = 0 + self.need_redraw = True + self.colours = None + self.collist = {} + self.get_list = getlist + + self.connect("expose-event", self.redraw) + self.connect("configure-event", self.reconfig) + + self.connect("button_release_event", self.release) + self.connect("button_press_event", self.press) + self.set_events(gtk.gdk.EXPOSURE_MASK + | gtk.gdk.BUTTON_PRESS_MASK + | gtk.gdk.BUTTON_RELEASE_MASK + | gtk.gdk.STRUCTURE_MASK) + + # choose a font + fd = pango.FontDescription('sans 10') + fd.set_absolute_size(25 * pango.SCALE) + self.font = fd + met = self.get_pango_context().get_metrics(fd) + self.lineheight = (met.get_ascent() + met.get_descent()) / pango.SCALE + fd = pango.FontDescription('sans 5') + fd.set_absolute_size(15 * pango.SCALE) + self.smallfont = fd + self.selected = 0 + self.top = 0 + self.book = None + + self.smslist = [] + + self.queue_draw() + + + def set_book(self, book): + self.book = book + + def lines(self): + alloc = self.get_allocation() + lines = alloc.height / self.lineheight + return lines + + def reset_list(self): + self.selected = 0 + self.smslist = None + self.size_requested = 0 + self.refresh() + + def refresh(self): + self.need_redraw = True + self.queue_draw() + + def assign_colour(self, purpose, name): + self.collist[purpose] = name + + def reconfig(self, w, ev): + alloc = w.get_allocation() + if not self.pixbuf: + return + if alloc.width != self.width or alloc.height != self.height: + self.pixbuf = None + self.need_redraw = True + + def add_col(self, sym, col): + c = gtk.gdk.color_parse(col) + gc = self.window.new_gc() + gc.set_foreground(self.get_colormap().alloc_color(c)) + self.colours[sym] = gc + + def redraw(self, w, ev): + if self.colours == None: + self.colours = {} + for p in self.collist: + self.add_col(p, self.collist[p]) + self.bg = self.get_style().bg_gc[gtk.STATE_NORMAL] + + if self.need_redraw: + self.draw_buf() + + self.window.draw_drawable(self.bg, self.pixbuf, 0, 0, 0, 0, + self.width, self.height) + + def draw_buf(self): + self.need_redraw = False + if self.pixbuf == None: + alloc = self.get_allocation() + self.pixbuf = gtk.gdk.Pixmap(self.window, alloc.width, alloc.height) + self.width = alloc.width + self.height = alloc.height + self.pixbuf.draw_rectangle(self.bg, True, 0, 0, + self.width, self.height) + + if self.top > self.selected: + self.top = 0 + max = self.lines() + if self.smslist == None or \ + (self.top + max > len(self.smslist) and self.size_requested < self.top + max): + self.size_requested = self.top + max + self.smslist = self.get_list(self.top + max) + for i in range(len(self.smslist)): + if i < self.top: + continue + if i > self.top + max: + break + if i == self.selected: + col = self.colours['bg-selected'] + else: + col = self.colours['bg-%d'%(i%2)] + + self.pixbuf.draw_rectangle(col, + True, 0, (i-self.top)*self.lineheight, + self.width, self.lineheight) + self.draw_sms(self.smslist[i], (i - self.top) * self.lineheight) + + + def draw_sms(self, sms, yoff): + + self.modify_font(self.smallfont) + tm = time.strftime("%Y-%m-%d %H:%M:%S", sms.time[0:6]+(0,0,0)) + then = time.mktime(sms.time[0:6]+(0,0,-1)) + now = time.time() + if now > then: + diff = now - then + if diff < 99: + delta = "%02d sec ago" % diff + elif diff < 99*60: + delta = "%02d min ago" % (diff/60) + elif diff < 48*60*60: + delta = "%02dh%02dm ago" % ((diff/60/60), (diff/60)%60) + else: + delta = tm[0:10] + tm = delta + tm[10:] + + l = self.create_pango_layout(tm) + self.pixbuf.draw_layout(self.colours['time'], + 0, yoff, l) + co = sms.correspondent + if self.book: + cor = book_name(self.book, co) + if cor: + co = cor[0] + if sms.source == 'LOCAL': + col = self.colours['recipient'] + co = 'To ' + co + else: + col = self.colours['sender'] + co = 'From '+co + l = self.create_pango_layout(co) + self.pixbuf.draw_layout(col, + 0, yoff + self.lineheight/2, l) + self.modify_font(self.font) + t = sms.text.replace("\n", " ") + t = t.replace("\n", " ") + l = self.create_pango_layout(t) + if sms.state in ['DRAFT', 'NEW']: + col = self.colours['mesg-new'] + else: + col = self.colours['mesg'] + self.pixbuf.draw_layout(col, + 180, yoff, l) + + def press(self,w,ev): + row = int(ev.y / self.lineheight) + self.selected = self.top + row + if self.selected >= len(self.smslist): + self.selected = len(self.smslist) - 1 + if self.selected < 0: + self.selected = 0 + + l = self.lines() + self.top += row - l / 2 + if self.top >= len(self.smslist) - l: + self.top = len(self.smslist) - l + 1 + if self.top < 0: + self.top = 0 + + self.refresh() + + def release(self,w,ev): + pass + +def load_book(file): + try: + f = open(file) + except: + f = open('/home/neilb/home/mobile-numbers-jan-08') + rv = [] + for l in f: + x = l.split(';') + rv.append([x[0],x[1]]) + rv.sort(lambda x,y: cmp(x[0],y[0])) + return rv + +def book_lookup(book, name, num): + st=[]; mid=[] + for l in book: + if name.lower() == l[0][0:len(name)].lower(): + st.append(l) + elif l[0].lower().find(name.lower()) >= 0: + mid.append(l) + st += mid + if len(st) == 0: + return [None, None] + if num >= len(st): + num = -1 + return st[num] + +def book_parse(book, name): + if not book: + return None + cnt = 0 + while len(name) and name[-1] == '.': + cnt += 1 + name = name[0:-1] + return book_lookup(book, name, cnt) + + + +def book_name(book, num): + if len(num) < 8: + return None + for ad in book: + if len(ad[1]) >= 8 and num[-8:] == ad[1][-8:]: + return ad + return None + +def book_speed(book, sym): + i = book_lookup(book, sym, 0) + if i[0] == None or i[0] != sym: + return None + j = book_lookup(book, i[1], 0) + if j[0] == None: + return (i[1], i[0]) + return (j[1], j[0]) + +def name_lookup(book, str): + # We need to report + # - a number - to dial + # - optionally a name that is associated with that number + # - optionally a new name to save the number as + # The name is normally alpha, but can be a single digit for + # speed-dial + # Dots following a name allow us to stop through multiple matches. + # So input can be: + # A single symbol. + # This is a speed dial. It maps to name, then number + # A string of >1 digits + # This is a literal number, we look up name if we can + # A string of dots + # This is a look up against recent incoming calls + # We look up name in phone book + # A string starting with alpha, possibly ending with dots + # This is a regular lookup in the phone book + # A number followed by a string + # This provides the string as a new name for saving + # A string of dots followed by a string + # This also provides the string as a newname + # An alpha string, with dots, followed by '+'then a single symbol + # This saves the match as a speed dial + # + # We return a triple of (number,oldname,newname) + if re.match('^[A-Za-z0-9]$', str): + # Speed dial lookup + s = book_speed(book, str) + if s: + return (s[0], s[1], None) + return None + m = re.match('^(\+?\d+)([A-Za-z][A-Za-z0-9 ]*)?$', str) + if m: + # Number and possible newname + s = book_name(book, m.group(1)) + if s: + return (m.group(1), s[0], m.group(2)) + else: + return (m.group(1), None, m.group(2)) + m = re.match('^([A-Za-z][A-Za-z0-9 ]*)(\.*)(\+[A-Za-z0-9])?$', str) + if m: + # name and dots + speed = None + if m.group(3): + speed = m.group(3)[1] + i = book_lookup(book, m.group(1), len(m.group(2))) + if i[0]: + return (i[1], i[0], speed) + return None + +class SendSMS(gtk.Window): + def __init__(self, store): + gtk.Window.__init__(self) + self.set_default_size(480,640) + self.set_title("SendSMS") + self.store = store + self.connect('destroy', self.close_win) + + self.selecting = False + self.viewing = False + self.book = None + self.create_ui() + + self.show() + self.reload_book = True + self.number = None + self.cutbuffer = None + + d = dnotify.dir(store.dirname) + self.watcher = d.watch('newmesg', lambda f : self.got_new()) + + self.watch_clip('sms-new') + + self.connect('property-notify-event', self.newprop) + self.add_events(gtk.gdk.PROPERTY_CHANGE_MASK) + def newprop(self, w, ev): + if ev.atom == '_INPUT_TEXT': + str = self.window.property_get('_INPUT_TEXT') + self.numentry.set_text(str[2]) + + def close_win(self, *a): + # FIXME save draft + gtk.main_quit() + + def create_ui(self): + + fd = pango.FontDescription("sans 10") + fd.set_absolute_size(25*pango.SCALE) + self.button_font = fd + v = gtk.VBox() ;v.show() ; self.add(v) + + self.sender = self.send_ui() + v.add(self.sender) + self.sender.hide() + + self.listing = self.list_ui() + v.add(self.listing) + self.listing.show() + + self.book = load_book("/data/address-book") + self.listview.set_book(self.book) + + self.rotate_list(self, target='All') + + def send_ui(self): + v = gtk.VBox() + main = v + + h = gtk.HBox() + h.show() + v.pack_start(h, expand=False) + l = gtk.Label('To:') + l.modify_font(self.button_font) + l.show() + h.pack_start(l, expand=False) + + self.numentry = FingerEntry() + h.pack_start(self.numentry) + self.numentry.modify_font(self.button_font) + self.numentry.show() + self.numentry.connect('changed', self.update_to); + + h = gtk.HBox() + l = gtk.Label('') + l.modify_font(self.button_font) + l.show() + h.pack_start(l) + h.show() + v.pack_start(h, expand=False) + h = gtk.HBox() + self.to_label = l + l = gtk.Label('0 chars') + l.modify_font(self.button_font) + l.show() + self.cnt_label = l + h.pack_end(l) + h.show() + v.pack_start(h, expand=False) + + h = gtk.HBox() + h.set_size_request(-1,80) + h.set_homogeneous(True) + h.show() + v.pack_start(h, expand=False) + + self.add_button(h, 'select', self.select) + self.add_button(h, 'clear', self.clear) + self.add_button(h, 'paste', self.paste) + + self.message = FingerText() + self.message.show() + self.message.modify_font(self.button_font) + sw = gtk.ScrolledWindow() ; sw.show() + sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + #v.add(self.message) + v.add(sw) + sw.add(self.message) + self.message.get_buffer().connect('changed', self.buff_changed) + + h = gtk.HBox() + h.set_size_request(-1,80) + h.set_homogeneous(True) + h.show() + v.pack_end(h, expand=False) + + self.add_button(h, 'Send GSM', self.send, 'GSM') + self.draft_button = self.add_button(h, 'Draft', self.draft) + self.add_button(h, 'Send EXE', self.send, 'EXE') + + return main + + def list_ui(self): + v = gtk.VBox() ; main = v + + h = gtk.HBox() ; h.show() + h.set_size_request(-1,80) + h.set_homogeneous(True) + v.pack_start(h, expand = False) + self.add_button(h, 'Del', self.delete) + self.view_button = self.add_button(h, 'View', self.view) + self.reply = self.add_button(h, 'New', self.open) + + h = gtk.HBox() ; h.show() + h.set_size_request(-1,80) + h.set_homogeneous(True) + v.pack_end(h, expand=False) + self.buttonA = self.add_button(h, 'Sent', self.rotate_list, 'A') + self.buttonB = self.add_button(h, 'Recv', self.rotate_list, 'B') + + + self.last_response = gtk.Label('') + v.pack_end(self.last_response, expand = False) + + h = gtk.HBox() ; h.show() + v.pack_start(h, expand=False) + b = gtk.Button("clr") ; b.show() + b.connect('clicked', self.clear_search) + h.pack_end(b, expand=False) + l = gtk.Label('search:') ; l.show() + h.pack_start(l, expand=False) + + e = gtk.Entry() ; e.show() + self.search_entry = e + h.pack_start(e) + + self.listview = SMSlist(self.load_list) + self.listview.show() + self.listview.assign_colour('time', 'blue') + self.listview.assign_colour('sender', 'red') + self.listview.assign_colour('recipient', 'black') + self.listview.assign_colour('mesg', 'black') + self.listview.assign_colour('mesg-new', 'red') + self.listview.assign_colour('bg-0', 'yellow') + self.listview.assign_colour('bg-1', 'pink') + self.listview.assign_colour('bg-selected', 'white') + + v.add(self.listview) + + self.singleview = gtk.TextView() + self.singleview.modify_font(self.button_font) + self.singleview.show() + self.singleview.set_wrap_mode(gtk.WRAP_WORD_CHAR) + sw = gtk.ScrolledWindow() + sw.add(self.singleview) + sw.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC) + sw.hide() + v.add(sw) + self.singlescroll = sw + + + main.show() + return main + + def add_button(self, parent, label, func, *args): + b = gtk.Button(label) + b.child.modify_font(self.button_font) + b.connect('clicked', func, *args) + b.set_property('can-focus', False) + parent.add(b) + b.show() + return b + + def update_to(self, w): + n = w.get_text() + if n == '': + self.reload_book = True + self.to_label.set_text('') + else: + if self.reload_book: + self.reload_book = False + self.book = load_book("/data/address-book") + self.listview.set_book(self.book) + e = name_lookup(self.book, n) + if e and e[1]: + self.to_label.set_text(e[1] + + ' '+ + e[0]) + self.number = e[0] + else: + self.to_label.set_text('??') + self.number = n + self.buff_changed(None) + + def buff_changed(self, w): + if self.numentry.get_text() == '' and self.message.get_buffer().get_property('text') == '': + self.draft_button.child.set_text('Cancel') + else: + self.draft_button.child.set_text('SaveDraft') + l = len(self.message.get_buffer().get_property('text')) + if l <= 160: + m = 1 + else: + m = (l+152)/153 + self.cnt_label.set_text('%d chars / %d msgs' % (l, m)) + + def select(self, w, *a): + if not self.selecting: + self.message.input.active = False + w.child.set_text('Cut') + self.selecting = True + else: + self.message.input.active = True + w.child.set_text('Select') + self.selecting = False + b = self.message.get_buffer() + bound = b.get_selection_bounds() + if bound: + (s,e) = bound + t = b.get_text(s,e) + self.cutbuffer = t + b.delete_selection(True, True) + + def clear(self, *a): + w = self.get_toplevel().get_focus() + if w == None: + w = self.message + if w == self.message: + self.cutbuffer = self.message.get_buffer().get_property('text') + b = self.message.get_buffer() + b.set_text('') + else: + self.cutbuffer = w.get_text() + w.set_text('') + + def paste(self, *a): + w = self.get_toplevel().get_focus() + if w == None: + w = self.message + if self.cutbuffer: + w.insert_at_cursor(self.cutbuffer) + pass + + def watch_clip(self, board): + self.cb = gtk.Clipboard(selection=board) + self.targets = [ (gtk.gdk.SELECTION_TYPE_STRING, 0, 0) ] + self.cb.set_with_data(self.targets, self.get, self.got_clip, None) + + def got_clip(self, clipb, data): + a = clipb.wait_for_text() + print "sms got clip", a + self.numentry.set_text(a) + self.listing.hide() + self.sender.show() + self.cb.set_with_data(self.targets, self.get, self.got_clip, None) + self.present() + + def get(self, sel, info, data): + sel.set_text("Number Please") + + def send(self, w, style): + sender = '0403463349' + recipient = self.number + mesg = self.message.get_buffer().get_property('text') + if not mesg or not recipient: + return + try: + if style == 'EXE': + p = Popen(['exesms', sender, recipient, mesg], stdout = PIPE) + else: + p = Popen(['gsm-sms', sender, recipient, mesg], stdout = PIPE) + except: + rv = 1 + line = 'Fork Failed' + else: + line = 'no response' + rv = p.wait() + for l in p.stdout: + if l: + line = l + + s = SMSmesg(to = recipient, text = mesg) + + if rv or line[0:2] != 'OK': + s.state = 'DRAFT' + target = 'Draft' + else: + target = 'All' + self.store.store(s) + self.last_response.set_text('Mess Send: '+ line.strip()) + self.last_response.show() + + self.sender.hide() + self.listing.show() + self.rotate_list(target=target) + + def draft(self, *a): + sender = '0403463349' + recipient = self.numentry.get_text() + if recipient: + rl = [recipient] + else: + rl = [] + mesg = self.message.get_buffer().get_property('text') + if mesg: + s = SMSmesg(to = recipient, text = mesg, state = 'DRAFT') + self.store.store(s) + self.sender.hide() + self.listing.show() + self.rotate_list(target='Draft') + def config(self, *a): + pass + def delete(self, *a): + if len(self.listview.smslist ) < 1: + return + s = self.listview.smslist[self.listview.selected] + self.store.delete(s) + sel = self.listview.selected + self.rotate_list(target=self.display_list) + self.listview.selected = sel + if self.viewing: + self.view(self.view_button) + + def view(self, w, *a): + if self.viewing: + w.child.set_text('View') + self.viewing = False + self.singlescroll.hide() + self.listview.show() + if self.listview.smslist and len(self.listview.smslist ) >= 1: + s = self.listview.smslist[self.listview.selected] + if s.state == 'NEW': + self.store.setstate(s, None) + if self.display_list == 'New': + self.rotate_list(target='New') + self.reply.child.set_text('New') + else: + if not self.listview.smslist or len(self.listview.smslist ) < 1: + return + s = self.listview.smslist[self.listview.selected] + w.child.set_text('List') + self.viewing = True + self.last_response.hide() + self.listview.hide() + if self.book: + n = book_name(self.book, s.correspondent) + if n and n[0]: + n = n[0] + ' ['+s.correspondent+']' + else: + n = s.correspondent + else: + n = s.correspondent + if s.source == 'LOCAL': + t = 'To: ' + n + '\n' + else: + t = 'From: %s (%s)\n' % (n, s.source) + tm = time.strftime('%d%b%Y %H:%M:%S', s.time[0:6]+(0,0,0)) + t += 'Time: ' + tm + '\n' + t += '\n' + t += s.text + self.singleview.get_buffer().set_text(t) + self.singlescroll.show() + + if s.source == 'LOCAL': + self.reply.child.set_text('Open') + else: + self.reply.child.set_text('Reply') + + def open(self, *a): + if self.viewing: + if len(self.listview.smslist) < 1: + return + s = self.listview.smslist[self.listview.selected] + if s.state == 'NEW': + self.store.setstate(s, None) + + self.numentry.set_text(s.correspondent) + self.message.get_buffer().set_text(s.text) + self.draft_button.child.set_text('SaveDraft') + else: + self.numentry.set_text('') + self.message.get_buffer().set_text('') + self.draft_button.child.set_text('Cancel') + self.listing.hide() + self.sender.show() + + def load_list(self, lines): + now = time.time() + l = [] + target = self.display_list + patn = self.search_entry.get_text() + #print 'pattern is', patn + if target == 'New': + (now, l) = self.store.lookup(now, 'NEW') + elif target == 'Draft': + (now, l) = self.store.lookup(now, 'DRAFT') + else: + if lines == 0: lines = 20 + while now and len(l) < lines: + (now, l2) = self.store.lookup(now) + for e in l2: + if patn and patn not in e.correspondent: + continue + if target == 'All': + l.append(e) + elif target == 'Sent' and e.source == 'LOCAL': + l.append(e) + elif target == 'Recv' and e.source != 'LOCAL': + l.append(e) + return l + + def rotate_list(self, w=None, ev=None, which = None, target=None): + # lists are: + # All, Recv, New, Sent, Draft + # When one is current, two others can be selected + + if target == None: + if w == None: + target = self.display_list + else: + target = w.child.get_text() + + if target == 'All': + self.buttonA.child.set_text('Sent') + self.buttonB.child.set_text('Recv') + if target == 'Sent': + self.buttonA.child.set_text('All') + self.buttonB.child.set_text('Draft') + if target == 'Draft': + self.buttonA.child.set_text('All') + self.buttonB.child.set_text('Sent') + if target == 'Recv': + self.buttonA.child.set_text('All') + self.buttonB.child.set_text('New') + if target == 'New': + self.buttonA.child.set_text('All') + self.buttonB.child.set_text('Recv') + + self.display_list = target + self.listview.reset_list() + + def clear_search(self, *a): + pass + + def got_new(self): + self.rotate_list(self, target = 'New') + +def main(args): + for p in ['/data','/media/card','/var/tmp']: + if os.path.exists(p): + pth = p + break + w = SendSMS(SMSstore(pth+'/SMS')) + gtk.settings_get_default().set_long_property("gtk-cursor-blink", 0, "main") + + gtk.main() + +if __name__ == '__main__': + main(sys.argv) diff --git a/sms/storesms.py b/sms/storesms.py new file mode 100644 index 0000000..f0d5f2e --- /dev/null +++ b/sms/storesms.py @@ -0,0 +1,486 @@ +# +# FIXME +# - trim newmesg and draft when possible. +# - remove old multipart files +# +# Store SMS messages is a bunch of files, one per month. +# Each message is stored on one line with space separated . +# URL encoding (%XX) is used to quote white space, unprintables etc +# We store 5 fields: +# - time stamp that we first saw the message. This is in UTC. +# This is the primary key. If a second message is seen in the same second, +# we quietly add 1 to the second. +# - Source, one of 'LOCAL' for locally composed, 'GSM' for recieved via GSM +# or maybe 'EMAIL' if received via email?? +# - Time message was sent, Localtime with -TZ. For GSM messages this comes with the +# message. For 'LOCAL' it might be '-', or will be the time we succeeded +# in sending. +# time is stored as a tupple (Y m d H M S Z) where Z is timezone in multiples +# of 15 minutes. +# - The correspondent: sender if GSM, recipient if LOCAL, or '-' if not sent. +# This might be a comma-separated list of recipients. +# - The text of the message +# +# Times are formatted %Y%m%d-%H%M%S and local time has a GSM TZ suffix. +# GSM TZ is from +48 to -48 in units of 15 minutes. (0 is +00) +# +# We never modify a message once it has been stored. +# If we have a draft that we edit and send, we delete the draft and +# create a new sent-message +# If we forward a message, we will then have two copies. +# +# New messages are not distinguished by a flag (which would have to be cleared) +# but by being in a separate list of new messages. +# We havea list of 'new' messages and a list of 'draft' messages. +# +# Multi-part messages are accumulated as they are received. The quoted message +# contains text for each part of the message. +# e.g. <1><2>nd%20so%20no.....<3> +# if we only have part 2 of 3. +# For each incomplete message there is a file (like 'draft' and 'newmesg') named +# for the message which provides an index to each incomplete message. +# It will be named e.g. 'multipart-1C' when 1C is the message id. +# +# This module defines 2 classes: +# SMSmesg +# This holds a message and so has timestamp, source, time, correspondent +# and text fields. These are decoded. +# SMSmesg also has 'state' which can be one of "NEW", "DRAFT" or None +# Finally it might have a 'ref' and a 'part' which is a tuple (this,max) +# This is only used when storing the message to link it up with +# a partner +# +# SMSstore +# This represents a collection of messages in a directory (one file per month) +# and provides lists of 'NEW' and 'DRAFT' messages. +# Operations: +# store(SMSmesg, NEW|DRAFT|) -> None +# stores the message and sets the timestamp +# lookup(latest-time, NEW|DRAFT|ALL) -> (earlytime, [SMSmesg]) +# collects a list of messages in reverse time order with times no later +# than 'latest-time'. Only consider NEW or DRAFT or ALL messages. +# The list may not be complete (typically one month at a time are returnned) +# If you want more, call again with 'earlytime' as 'latest-time'). +# delete(SMSmesg) +# delete the given message (based on the timestamp only) +# setstate(SMSmesg, NEW|DRAFT|None) +# update the 'new' and 'draft' lists or container, or not container, this +# message. +# +# + +import os, fcntl, re, time, urllib + +def umktime(tm): + # like time.mktime, but tm is UTC + # So we want a 't' where + # time.gmtime(t)[0:6] == tm[0:6] + estimate = time.mktime(tm) - time.timezone + t2 = time.gmtime(estimate) + while t2[0:6] < tm[0:6]: + estimate += 15*60 + t2 = time.gmtime(estimate) + while t2[0:6] > tm[0:6]: + estimate -= 15*60 + t2 = time.gmtime(estimate) + return estimate + +def parse_time(strg): + return int(umktime(time.strptime(strg, "%Y%m%d-%H%M%S"))) +def parse_ltime(strg): + n=3 + if strg[-2] == '+' or strg[-2] == '-': + n=2 + z = strg[-n:] + return time.strptime(strg[:-n], "%Y%m%d-%H%M%S")[0:6] + (int(z),) +def format_time(t): + return time.strftime("%Y%m%d-%H%M%S", time.gmtime(t)) +def format_ltime(tm): + return time.strftime("%Y%m%d-%H%M%S", tm[0:6]+(0,0,0)) + ("%+03d" % tm[6]) + + +class SMSmesg: + def __init__(self, **a): + if len(a) == 1 and 'line' in a: + # line read from a file, with 5 fields. + # stamp, source, time, correspondent, text + line = a['line'].split() + self.stamp = parse_time(line[0]) + self.source = line[1] + self.time = parse_ltime(line[2]) + self.correspondents = [] + for c in line[3].split(','): + if c != '-': + self.correspondents.append(urllib.unquote(c)) + self.set_corresponent() + self.state = None + + self.parts = None + txt = line[4] + if txt[0] != '<': + self.text = urllib.unquote(txt) + return + # multipart: <1>text...<2>text...<3><4> + m = re.findall('<(\d+)>([^<]*)', txt) + parts = [] + for (pos, strg) in m: + p = int(pos) + while len(parts) < p: + parts.append(None) + if strg: + parts[p-1] = urllib.unquote(strg) + self.parts = parts + self.reduce_parts() + else: + self.stamp = int(time.time()) + self.source = None + lt = time.localtime() + z = time.timezone/15/60 + if lt[8] == 1: + z -= 4 + self.time = time.localtime()[0:6] + (z,) + self.correspondents = [] + self.text = "" + self.state = None + self.ref = None + self.parts = None + part = None + for k in a: + if k == 'stamp': + self.stamp = a[k] + elif k == 'source': + self.source = a[k] + elif k == 'time': + # time can be a GSM string: 09/02/09,09:56:28+44 (ymd,HMS+z) + # or a tuple (y,m,d,H,M,S,z) + if type(a[k]) == str: + t = a[k][:-3] + z = a[k][-3:] + tm = time.strptime(t, "%y/%m/%d,%H:%M:%S") + self.time = tm[0:6] + (int(z),) + elif k == 'to' or k == 'sender': + if self.source == None: + if k == 'to': + self.source = 'LOCAL' + if k == 'sender': + self.source = 'GSM' + self.correspondents = [ a[k] ] + elif k == 'correspondents': + self.correspondents = a[k] + elif k == 'text': + self.text = a[k] + elif k == 'state': + self.state = a[k] + elif k == 'ref': + if a[k] != None: + self.ref = a[k] + elif k == 'part': + if a[k]: + part = a[k] + else: + raise ValueError + if self.source == None: + self.source = 'LOCAL' + if part: + print 'part', part[0], part[1] + self.parts = [None for x in range(part[1])] + self.parts[part[0]-1] = self.text + self.reduce_parts() + self.set_corresponent() + + self.month_re = re.compile("^[0-9]{6}$") + + def reduce_parts(self): + def reduce_pair(a,b): + if b == None: + b = "...part of message missing..." + if a == None: + return b + return a+b + self.text = reduce(reduce_pair, self.parts) + + + def set_corresponent(self): + if len(self.correspondents) == 1: + self.correspondent = self.correspondents[0] + elif len(self.correspondents) == 0: + self.correspondent = "Unsent" + else: + self.correspondent = "Multiple" + + def format(self): + fmt = "%s %s %s %s " % (format_time(self.stamp), self.source, + format_ltime(self.time), + self.format_correspondents()) + if not self.parts: + return fmt + urllib.quote(self.text) + + for i in range(len(self.parts)): + fmt += ("<%d>" % (i+1)) + urllib.quote(self.parts[i]) if self.parts[i] else "" + return fmt + + def format_correspondents(self): + r = "" + for i in self.correspondents: + if i: + r += ',' + urllib.quote(i) + if r: + return r[1:] + else: + return '-' + + +class SMSstore: + def __init__(self, dir): + self.month_re = re.compile("^[0-9]{6}$") + self.cached_month = None + self.dirname = dir + # find message files + self.set_files() + self.drafts = self.load_list('draft') + self.newmesg = self.load_list('newmesg') + + def load_list(self, name, update = None, *args): + + l = [] + try: + f = open(self.dirname + '/' + name, 'r+') + except IOError: + return l + + if update: + fcntl.lockf(f, fcntl.LOCK_EX) + for ln in f: + l.append(parse_time(ln.strip())) + l.sort() + l.reverse() + + if update and update(l, *args): + f2 = open(self.dirname + '/' + name + '.new', 'w') + for t in l: + f2.write(format_time(t)+"\n") + f2.close() + os.rename(self.dirname + '/' + name + '.new', + self.dirname + '/' + name) + f.close() + return l + + def load_month(self, f): + # load the messages from f, which is open for read + rv = {} + for l in f: + l.strip() + m = SMSmesg(line=l) + rv[m.stamp] = m + if m.stamp in self.drafts: + m.state = 'DRAFT' + elif m.stamp in self.newmesg: + m.state = 'NEW' + return rv + + def store_month(self, l, m): + dm = self.dirname + '/' + m + f = open(dm+'.new', 'w') + for s in l: + f.write(l[s].format() + "\n") + f.close() + os.rename(dm+'.new', dm) + if not m in self.files: + self.files.append(m) + self.files.sort() + self.files.reverse() + if self.cached_month == m: + self.cache = l + + def store(self, sms): + orig = None + if sms.ref != None: + # This is part of a multipart. + # If there already exists part of this + # merge them together + # + times = self.load_list('multipart-' + sms.ref) + if len(times) == 1: + orig = self.load(times[0]) + if orig and orig.parts: + for i in range(len(sms.parts)): + if sms.parts[i] == None and i < len(orig.parts): + sms.parts[i] = orig.parts[i] + else: + orig = None + + m = time.strftime("%Y%m", time.gmtime(sms.stamp)) + try: + f = open(self.dirname + '/' + m, "r+") + except: + f = open(self.dirname + '/' + m, "w+") + complete = True + if sms.ref != None: + for i in sms.parts: + if i == None: + complete = False + if complete: + sms.reduce_parts() + sms.parts = None + + fcntl.lockf(f, fcntl.LOCK_EX) + l = self.load_month(f) + while sms.stamp in l: + sms.stamp += 1 + l[sms.stamp] = sms + self.store_month(l, m); + f.close() + + if orig: + self.delete(orig) + if sms.ref != None: + if complete: + try: + os.unlink(self.dirname + '/multipart-' + sms.ref) + except: + pass + elif orig: + def replacewith(l, tm): + while len(l): + l.pop() + l.append(tm) + return True + self.load_list('multipart-' + sms.ref, replacewith, sms.stamp) + else: + f = open(self.dirname +'/multipart-' + sms.ref, 'w') + fcntl.lockf(f, fcntl.LOCK_EX) + f.write(format_time(sms.stamp) + '\n') + f.close() + + if sms.state == 'NEW' or sms.state == 'DRAFT': + s = 'newmesg' + if sms.state == 'DRAFT': + s = 'draft' + f = open(self.dirname +'/' + s, 'a') + fcntl.lockf(f, fcntl.LOCK_EX) + f.write(format_time(sms.stamp) + '\n') + f.close() + elif sms.state != None: + raise ValueError + + def set_files(self): + self.files = [] + for f in os.listdir(self.dirname): + if self.month_re.match(f): + self.files.append(f) + self.files.sort() + self.files.reverse() + + def lookup(self, lasttime = None, state = None): + if lasttime == None: + lasttime = int(time.time()) + if state == None: + return self.getmesgs(lasttime) + if state == 'DRAFT': + self.drafts = self.load_list('draft') + times = self.drafts + elif state == 'NEW': + self.newmesg = self.load_list('newmesg') + times = self.newmesg + else: + raise ValueError + + self.set_files() + self.cached_month = None + self.cache = None + rv = [] + for t in times: + if t > lasttime: + continue + s = self.load(t) + if s: + s.state = state + rv.append(s) + return(0, rv) + + def getmesgs(self, last): + rv = [] + for m in self.files: + t = parse_time(m + '01-000000') + if t > last: + continue + mon = self.load_month(open(self.dirname + '/' + m)) + for mt in mon: + if mt <= last: + rv.append(mon[mt]) + if rv: + rv.sort(cmp = lambda x,y:cmp(y.stamp, x.stamp)) + return (t-1, rv) + return (0, []) + + def load(self, t): + m = time.strftime("%Y%m", time.gmtime(t)) + if not m in self.files: + return None + if self.cached_month != m: + self.cached_month = m + self.cache = self.load_month(open(self.dirname + '/' + m)) + if t in self.cache: + return self.cache[t] + return None + + def delete(self, msg): + if isinstance(msg, SMSmesg): + tm = msg.stamp + else: + tm = msg + m = time.strftime("%Y%m", time.gmtime(tm)) + try: + f = open(self.dirname + '/' + m, "r+") + except: + return + + fcntl.lockf(f, fcntl.LOCK_EX) + l = self.load_month(f) + if tm in l: + del l[tm] + self.store_month(l, m); + f.close() + + def del1(l, tm): + if tm in l: + l.remove(tm) + return True + return False + + self.drafts = self.load_list('draft', del1, tm) + self.newmesg = self.load_list('newmesg', del1, tm) + + def setstate(self, msg, state): + tm = msg.stamp + + def del1(l, tm): + if tm in l: + l.remove(tm) + return True + return False + + if tm in self.drafts and state != 'DRAFT': + self.drafts = self.load_list('draft', del1, tm) + if tm in self.newmesg and state != 'NEW': + self.newmesg = self.load_list('newmesg', del1, tm) + + if tm not in self.drafts and state == 'DRAFT': + f = open(self.dirname +'/draft', 'a') + fcntl.lockf(f, fcntl.LOCK_EX) + f.write(format_time(sms.stamp) + '\n') + f.close() + self.drafts.append(tm) + self.drafts.sort() + self.drafts.reverse() + + if tm not in self.newmesg and state == 'NEW': + f = open(self.dirname +'/newmesg', 'a') + fcntl.lockf(f, fcntl.LOCK_EX) + f.write(format_time(sms.stamp) + '\n') + f.close() + self.newmesg.append(tm) + self.newmesg.sort() + self.newmesg.reverse() + + diff --git a/sound/list.h b/sound/list.h new file mode 100644 index 0000000..8626630 --- /dev/null +++ b/sound/list.h @@ -0,0 +1,289 @@ +/* + * Copied from the Linux kernel source tree, version 2.6.0-test1. + * + * Licensed under the GPL v2 as per the whole kernel source tree. + * + */ + +#ifndef _LIST_H +#define _LIST_H + +/** + * container_of - cast a member of a structure out to the containing structure + * + * @ptr: the pointer to the member. + * @type: the type of the container struct this is embedded in. + * @member: the name of the member within the struct. + * + */ +#define container_of(ptr, type, member) ({ \ + const typeof( ((type *)0)->member ) *__mptr = (ptr); \ + (type *)( (char *)__mptr - offsetof(type,member) );}) + +/* + * These are non-NULL pointers that will result in page faults + * under normal circumstances, used to verify that nobody uses + * non-initialized list entries. + */ +#define LIST_POISON1 ((void *) 0x00100100) +#define LIST_POISON2 ((void *) 0x00200200) + +/* + * Simple doubly linked list implementation. + * + * Some of the internal functions ("__xxx") are useful when + * manipulating whole lists rather than single entries, as + * sometimes we already know the next/prev entries and we can + * generate better code by using them directly rather than + * using the generic single-entry routines. + */ + +struct list_head { + struct list_head *next, *prev; +}; + +#define LIST_HEAD_INIT(name) { &(name), &(name) } + +#define LIST_HEAD(name) \ + struct list_head name = LIST_HEAD_INIT(name) + +#define INIT_LIST_HEAD(ptr) do { \ + (ptr)->next = (ptr); (ptr)->prev = (ptr); \ +} while (0) + +/* + * Insert a new entry between two known consecutive entries. + * + * This is only for internal list manipulation where we know + * the prev/next entries already! + */ +static inline void __list_add(struct list_head *new, + struct list_head *prev, + struct list_head *next) +{ + next->prev = new; + new->next = next; + new->prev = prev; + prev->next = new; +} + +/** + * list_add - add a new entry + * @new: new entry to be added + * @head: list head to add it after + * + * Insert a new entry after the specified head. + * This is good for implementing stacks. + */ +static inline void list_add(struct list_head *new, struct list_head *head) +{ + __list_add(new, head, head->next); +} + +/** + * list_add_tail - add a new entry + * @new: new entry to be added + * @head: list head to add it before + * + * Insert a new entry before the specified head. + * This is useful for implementing queues. + */ +static inline void list_add_tail(struct list_head *new, struct list_head *head) +{ + __list_add(new, head->prev, head); +} + +/* + * Delete a list entry by making the prev/next entries + * point to each other. + * + * This is only for internal list manipulation where we know + * the prev/next entries already! + */ +static inline void __list_del(struct list_head * prev, struct list_head * next) +{ + next->prev = prev; + prev->next = next; +} + +/** + * list_del - deletes entry from list. + * @entry: the element to delete from the list. + * Note: list_empty on entry does not return true after this, the entry is + * in an undefined state. + */ +static inline void list_del(struct list_head *entry) +{ + __list_del(entry->prev, entry->next); + entry->next = LIST_POISON1; + entry->prev = LIST_POISON2; +} + +/** + * list_del_init - deletes entry from list and reinitialize it. + * @entry: the element to delete from the list. + */ +static inline void list_del_init(struct list_head *entry) +{ + __list_del(entry->prev, entry->next); + INIT_LIST_HEAD(entry); +} + +/** + * list_move - delete from one list and add as another's head + * @list: the entry to move + * @head: the head that will precede our entry + */ +static inline void list_move(struct list_head *list, struct list_head *head) +{ + __list_del(list->prev, list->next); + list_add(list, head); +} + +/** + * list_move_tail - delete from one list and add as another's tail + * @list: the entry to move + * @head: the head that will follow our entry + */ +static inline void list_move_tail(struct list_head *list, + struct list_head *head) +{ + __list_del(list->prev, list->next); + list_add_tail(list, head); +} + +/** + * list_empty - tests whether a list is empty + * @head: the list to test. + */ +static inline int list_empty(struct list_head *head) +{ + return head->next == head; +} + +static inline void __list_splice(struct list_head *list, + struct list_head *head) +{ + struct list_head *first = list->next; + struct list_head *last = list->prev; + struct list_head *at = head->next; + + first->prev = head; + head->next = first; + + last->next = at; + at->prev = last; +} + +/** + * list_splice - join two lists + * @list: the new list to add. + * @head: the place to add it in the first list. + */ +static inline void list_splice(struct list_head *list, struct list_head *head) +{ + if (!list_empty(list)) + __list_splice(list, head); +} + +/** + * list_splice_init - join two lists and reinitialise the emptied list. + * @list: the new list to add. + * @head: the place to add it in the first list. + * + * The list at @list is reinitialised + */ +static inline void list_splice_init(struct list_head *list, + struct list_head *head) +{ + if (!list_empty(list)) { + __list_splice(list, head); + INIT_LIST_HEAD(list); + } +} + +/** + * list_entry - get the struct for this entry + * @ptr: the &struct list_head pointer. + * @type: the type of the struct this is embedded in. + * @member: the name of the list_struct within the struct. + */ +#define list_entry(ptr, type, member) \ + container_of(ptr, type, member) + +/** + * list_for_each - iterate over a list + * @pos: the &struct list_head to use as a loop counter. + * @head: the head for your list. + */ +#define list_for_each(pos, head) \ + for (pos = (head)->next; pos != (head); \ + pos = pos->next) + +/** + * __list_for_each - iterate over a list + * @pos: the &struct list_head to use as a loop counter. + * @head: the head for your list. + * + * This variant differs from list_for_each() in that it's the + * simplest possible list iteration code. + * Use this for code that knows the list to be very short (empty + * or 1 entry) most of the time. + */ +#define __list_for_each(pos, head) \ + for (pos = (head)->next; pos != (head); pos = pos->next) + +/** + * list_for_each_prev - iterate over a list backwards + * @pos: the &struct list_head to use as a loop counter. + * @head: the head for your list. + */ +#define list_for_each_prev(pos, head) \ + for (pos = (head)->prev; pos != (head); pos = pos->prev) + +/** + * list_for_each_safe - iterate over a list safe against removal of list entry + * @pos: the &struct list_head to use as a loop counter. + * @n: another &struct list_head to use as temporary storage + * @head: the head for your list. + */ +#define list_for_each_safe(pos, n, head) \ + for (pos = (head)->next, n = pos->next; pos != (head); \ + pos = n, n = pos->next) + +/** + * list_for_each_entry - iterate over list of given type + * @pos: the type * to use as a loop counter. + * @head: the head for your list. + * @member: the name of the list_struct within the struct. + */ +#define list_for_each_entry(pos, head, member) \ + for (pos = list_entry((head)->next, typeof(*pos), member); \ + &pos->member != (head); \ + pos = list_entry(pos->member.next, typeof(*pos), member)) + +/** + * list_for_each_entry_reverse - iterate backwards over list of given type. + * @pos: the type * to use as a loop counter. + * @head: the head for your list. + * @member: the name of the list_struct within the struct. + */ +#define list_for_each_entry_reverse(pos, head, member) \ + for (pos = list_entry((head)->prev, typeof(*pos), member); \ + &pos->member != (head); \ + pos = list_entry(pos->member.prev, typeof(*pos), member)) + +/** + * list_for_each_entry_safe - iterate over list of given type safe against removal of list entry + * @pos: the type * to use as a loop counter. + * @n: another type * to use as temporary storage + * @head: the head for your list. + * @member: the name of the list_struct within the struct. + */ +#define list_for_each_entry_safe(pos, n, head, member) \ + for (pos = list_entry((head)->next, typeof(*pos), member), \ + n = list_entry(pos->member.next, typeof(*pos), member); \ + &pos->member != (head); \ + pos = n, n = list_entry(n->member.next, typeof(*n), member)) + +#endif /* _LIST_H */ diff --git a/sound/notes b/sound/notes new file mode 100644 index 0000000..8f109ce --- /dev/null +++ b/sound/notes @@ -0,0 +1,102 @@ + + snd_config_t *config; + file = "/data/senarios/$CHOICE"; + err = snd_config_top(&config); + err = snd_input_stdio_open(&in, file, "r"); + err = snd_config_load(config, in); + snd_input_close(in); + cardno = snd_card_get_index(cardname); + + char name[32]; + snd_ctl_t *handle; + snd_ctl_card_info_t *info; + snd_ctl_card_info_alloca(&info); + sprintf(name, "hw:%d", cardno); + err = snd_ctl_open(&handle, name, 0); + err = snd_ctl_card_info(handle, info); + id = snd_ctl_card_info_get_id(info); + err = snd_config_searchv(config, &control, "state", id, "control", 0); + snd_config_for_each(i, next, control) { + snd_config_t *n = snd_config_iterator_entry(i); + snd_ctl_elem_value_alloca(&ctl); + snd_ctl_elem_info_alloca(&info); + err = snd_config_get_id(control, &id); + + + err = set_control(handle, n); + if (err < 0 && ! force_restore) + goto _close; + } + + +------------- + +Enhancements: + want + volume control + route to bluetooth instead + seek, and restart a second or two back + route between GSM and main/bluetooth + record input - set samples etc? + mix samples with routing + +separate directory. +When file appears there, recording is started +If a symlink appears that names an alsa device, mix the +recording in to that device. It should also be echo-cancelled +with the output if enabled for the devices +(we enable for main and bt) + +Cards are: gta04 gta04voice gta04headset (CARD=gta04). + +need 'plug:' to ensure sample rate conversion etc. +need 'dmix:' to allow other sounds to play. + + +We can have multiple devices open at once, which might use +dmix to output to the one device. +Each 'record' device can be paired with a 'play' device. +A record and a play can be marked for echo cancelling. +This mixes the 'play' stream into the 'record' stream so that +the echo disappears from it. For this to work, the 'record' +stream must be nearly synced with the 'play' stream. +There is no point having 'play' samples which haven't gone out yet +as they cannot have echo. Rather I want the samples that I just +played to mix with the samples that I just recored. + +However gsm-voice-routing doesn't do that. It mixes the 'just +recorded' on each path.?? + +I need to work in whole periods and I would like these to be +closely synced... Can I do that? +Not easily. I just need to keep the periods low so the sync difference +is small. + +gsm-voice-routing uses a period_size of 256 of 32msec. +oslec say 16ms or 32ms tail is fine. 64ms would cope with +out-of-sync buffers. + +So... + all processing must be async + several sounds with same priority can play at once + a sound file which contains 'gsm' triggers copying between + gta04voice and current device. + We start recording and don't start playing until a sample + is available. + +So the active things can be: + 'sound' which has a plug:dmix: device it is being written to + 'mic' which is the souce on the main device + 'gsmout' which 'mic' is being copied to + 'gsmin' which is being read in + 'speaker' which is another plug:dmix: + +So we can happily keep the voice routing in a separate process +and still allow mixing. + + + +Copying needs to wait for 'voice' to start delivering data. +So: + - configure all stream. + - \ No newline at end of file diff --git a/sound/sound.c b/sound/sound.c new file mode 100644 index 0000000..f2f0b4d --- /dev/null +++ b/sound/sound.c @@ -0,0 +1,816 @@ +/* TOFIX + * Record where up to - and total length + * Don't reconfigure between identical format sounds. + * handle seek for ogg vorbis + * ??handle MP3 in WAV files + * long gap between sounds.??? maybe all those zeros? + */ + +/* + * + * This is a daemon that waits for sound files to appear in a particular + * directory, and when they do, it plays them. + * Files can be WAV or OGG VORBIS + * If there are multiple files, the lexically first is played + * If a file has a suffix of -NNNNNNN, then play starts that many + * milliseconds in to the file. + * When a file disappear, play stops. + * When the end of the sound is reached the file (typically a link) is removed. + * However an empty file is treated as containing infinite silence, so + * it is never removed. + * When a new file appears which is lexically earlier than the one being + * played, the played file is suspended until the earlier files are finished + * with. + * The current-play position (in milliseconds) is occasionally written to a file + * with the same name as the sound file, but with a leading period. + * This file starts with 'P' if the sound is playing, and 'S' if it has + * stopped. When playing, the actual position can be calculated using + * the current time and mtime of the file. + * + * Expected use is that various alert tones are added to the directory with + * early names, and a music file can be added with a later name for general + * listening. + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA + * + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "list.h" +#include "libsus.h" + +#define WAV_RIFF COMPOSE_ID('R','I','F','F') +#define WAV_WAVE COMPOSE_ID('W','A','V','E') +#define WAV_FMT COMPOSE_ID('f','m','t',' ') +#define WAV_DATA COMPOSE_ID('d','a','t','a') +#define WAV_PCM_CODE 1 + +#if __BYTE_ORDER == __LITTLE_ENDIAN +#define COMPOSE_ID(a,b,c,d) ((a) | ((b)<<8) | ((c)<<16) | ((d)<<24)) +#define LE_SHORT(v) (v) +#define LE_INT(v) (v) +#define BE_SHORT(v) bswap_16(v) +#define BE_INT(v) bswap_32(v) +#elif __BYTE_ORDER == __BIG_ENDIAN +#define COMPOSE_ID(a,b,c,d) ((d) | ((c)<<8) | ((b)<<16) | ((a)<<24)) +#define LE_SHORT(v) bswap_16(v) +#define LE_INT(v) bswap_32(v) +#define BE_SHORT(v) (v) +#define BE_INT(v) (v) +#else +#error "Wrong endian" +#endif + +typedef struct { + u_int magic; /* 'RIFF' */ + u_int length; /* filelen */ + u_int type; /* 'WAVE' */ +} WaveHeader; + +typedef struct { + u_short format; /* should be 1 for PCM-code */ + u_short modus; /* 1 Mono, 2 Stereo */ + u_int sample_fq; /* frequence of sample */ + u_int byte_p_sec; + u_short byte_p_spl; /* samplesize; 1 or 2 bytes */ + u_short bit_p_spl; /* 8, 12 or 16 bit */ +} WaveFmtBody; + +typedef struct { + u_int type; /* 'data' or 'fmt ' */ + u_int length; /* samplecount */ +} WaveChunkHeader; + + +#ifndef LLONG_MAX +#define LLONG_MAX 9223372036854775807LL +#endif + +#define DEFAULT_FORMAT SND_PCM_FORMAT_U8 +#define DEFAULT_SPEED 8000 + +#define FORMAT_DEFAULT -1 +#define FORMAT_RAW 0 +#define FORMAT_VOC 1 +#define FORMAT_WAVE 2 +#define FORMAT_AU 3 +#define FORMAT_VORBIS 4 +#define FORMAT_UNKNOWN 9999 +/* global data */ + + +struct sound { + int fd; + int empty; + struct list_head list; + int seen; + char *name; + char scenario[20]; + int ino; + long posn; + int format; /* FORMAT_WAVE or FORMAT_OGG */ + char buf[8192]; + int period_bytes; + int bytes, bytes_used, last_read; + int eof; + + /* An audio file can contain multiple chunks. + * Here we record the remaining bytes expected + * before a header + */ + int chunk_bytes; + + OggVorbis_File vf; + + int pcm_format; + int sample_bytes; + int channels; + int rate; +}; + +struct dev { + snd_pcm_t *handle; + char *period_buf; + char scenario[20]; + int buf_size; + int period_bytes; + int sample_bytes; + int present; + int pcm_format, channels, rate; +}; + +int dir_changed = 1; + +void handle_change(int sig) +{ + dir_changed = 1; +} + +static void *raw_read(struct sound *s, int bytes) +{ + /* Return pointer to next 'bytes' bytes in the buffer. + * If there aren't that many, copy down and read some + * more. If we hit EOF, return NULL and set ->eof. + */ + while (s->bytes - s->bytes_used < bytes && !s->eof) { + if (s->bytes_used && + s->bytes_used < s->bytes) + memmove(s->buf, s->buf+s->bytes_used, + s->bytes - s->bytes_used); + s->bytes -= s->bytes_used; + s->bytes_used = 0; + while (s->bytes < bytes && !s->eof) { + int n = read(s->fd, s->buf+s->bytes, sizeof(s->buf) - s->bytes); + if (n <= 0) + s->eof = 1; + else + s->bytes += n; + } + } + if (s->bytes - s->bytes_used < bytes) + return NULL; + else { + void *rv = s->buf + s->bytes_used; + s->bytes_used += bytes; + s->last_read = bytes; + return rv; + } +} + +static void raw_unread(struct sound *s) +{ + s->bytes_used -= s->last_read; + s->last_read = 0; +} + +static void raw_skip(struct sound *s, int bytes) +{ + if (s->bytes - s->bytes_used >= bytes) { + s->bytes_used += bytes; + return; + } + bytes -= (s->bytes - s->bytes_used); + s->bytes = 0; + s->bytes_used = 0; + lseek(s->fd, bytes, SEEK_CUR); +} + + +int parse_wave(struct sound *s) +{ + WaveHeader *h; + WaveChunkHeader *c; + WaveFmtBody *f; + int n; + int len; + + h = raw_read(s, sizeof(*h)); + if (!h) + return 0; + if (h->magic != WAV_RIFF || h->type != WAV_WAVE) { + raw_unread(s); + return 0; + } + /* Ignore the length - wait for EOF */ + while (1) { + c = raw_read(s, sizeof(*c)); + if (!c) + return 0; + len = LE_INT(c->length); + len += len % 2; + if (c->type == WAV_FMT) + break; + raw_skip(s, len); + } + if (len < sizeof(WaveFmtBody)) + return 0; + f = raw_read(s, len); + if (!f) + return 0; + + if (LE_SHORT(f->format) == 1 && + LE_SHORT(f->bit_p_spl) == 16) { + s->pcm_format = SND_PCM_FORMAT_S16_LE; + s->sample_bytes = 2; + } else if (LE_SHORT(f->format) == 1 && + LE_SHORT(f->bit_p_spl) == 8) { + s->pcm_format = SND_PCM_FORMAT_U8; + s->sample_bytes = 1; + } else + return 0; + s->channels = LE_SHORT(f->modus); + if (s->channels < 1 || s->channels > 2) + return 0; + s->sample_bytes *= s->channels; + s->rate = LE_INT(f->sample_fq); + s->eof = 0; + s->chunk_bytes = 0; + return 1; +} + +static int read_wave(struct sound *s, char *buf, int bytes) +{ + WaveChunkHeader *h; + int len; + int b = 0; + char *rbuf; + int i; + + while (b < bytes) { + while (s->chunk_bytes == 0) { + h = raw_read(s, sizeof(*h)); + if (!h) + break; + len = LE_INT(h->length); + len += len % 2; + if (h->type != WAV_DATA) { + raw_skip(s, len); + continue; + } + s->chunk_bytes = len; + } + if (s->chunk_bytes == 0) + break; + if (s->chunk_bytes >= bytes - b) { + rbuf = raw_read(s, bytes - b); + if (!rbuf) + break; + s->chunk_bytes -= bytes - b; + memcpy(buf + b, rbuf, bytes - b); + return bytes; + } + /* Remainder of chunk is less than we need */ + rbuf = raw_read(s, s->chunk_bytes); + if (!rbuf) + break; + memcpy(buf + b, rbuf, s->chunk_bytes); + b += s->chunk_bytes; + s->chunk_bytes = 0; + } + + rbuf = buf; + for (i = b; i < bytes && s->chunk_bytes > 1; i++) { + char *bf = raw_read(s, 1); + if (bf) + buf[i] = bf[0]; + else + break; + s->chunk_bytes--; + } + return i; +} + +void seek_wave(struct sound *s, int msec) +{ + /* skip over 'msec' milliseconds of the wave */ + WaveChunkHeader *h; + int bytes = ((long long)msec * s->rate / 1000) * s->sample_bytes; + int len; + + while (bytes) { + while (s->chunk_bytes == 0) { + h = raw_read(s, sizeof(*h)); + if (!h) + break; + len = LE_INT(h->length); + len += len % 2; + if (h->type != WAV_DATA) { + raw_skip(s, len); + continue; + } + s->chunk_bytes = len; + } + if (s->chunk_bytes == 0) + break; + if (s->chunk_bytes >= bytes) { + raw_skip(s, bytes); + s->chunk_bytes -= bytes; + return; + } + raw_skip(s, s->chunk_bytes); + bytes -= s->chunk_bytes; + s->chunk_bytes = 0; + } +} + +static int parse_vorbis(struct sound *s) +{ + FILE *f; + vorbis_info *vi; + int rv; + + lseek(s->fd, 0, 0); + f = fdopen(s->fd, "r"); + if ((rv = ov_open_callbacks(f, &s->vf, NULL, 0, OV_CALLBACKS_DEFAULT)) < 0) { + fclose(f); + return 0; + } + + vi = ov_info(&s->vf,-1); + + s->pcm_format = SND_PCM_FORMAT_S16_LE; + s->channels = vi->channels; + s->rate = vi->rate; + s->sample_bytes = 2 * s->channels; + s->eof = 0; + return 1; +} + +static int read_vorbis(struct sound *s, char *buf, int len) +{ + int section; + int have = 0; + if (s->eof) + return 0; + + while(!s->eof && have < len) { + long ret = ov_read(&s->vf, + buf + have, + len - have, + 0,2,1,§ion); + if (ret == 0) { + /* EOF */ + s->eof=1; + } else if (ret < 0) { + /* error in the stream. Not a problem, just reporting it in + case we (the app) cares. In this case, we don't. */ + s->eof = 1; + } else { + /* we don't bother dealing with sample rate changes, etc, but + you'll have to*/ + have += ret; + } + } + + if (s->eof) + ov_clear(&s->vf); + return have; +} + +static void seek_vorbis(struct sound *s, int msec) +{ +} + + +snd_pcm_t *open_dev(void) +{ + snd_pcm_t *handle; + int rc; + + rc = snd_pcm_open(&handle, "default", SND_PCM_STREAM_PLAYBACK, 0/*SND_PCM_NONBLOCK*/); + if (rc < 0) + return NULL; + else + return handle; +} + +void dev_close(struct dev *dev); +void set_scenario(struct dev *dev, char *scenario) +{ + char path[100]; + if (scenario[0] == 0) + return; + if (strcmp(dev->scenario, scenario) == 0) + return; + dev_close(dev); + strcpy(dev->scenario, scenario); + snprintf(path, 100, "alsactl -f /data/scenarios/%s restore", scenario); + system(path); +} + +void set_params(struct dev *dev, struct sound *sound) +{ + snd_pcm_hw_params_t *hwp; + + if (sound->format == FORMAT_UNKNOWN) + return; + + set_scenario(dev, sound->scenario); + if (dev->handle == NULL) { + dev->handle = open_dev(); + dev->pcm_format = 0; + } + + if (dev->pcm_format == sound->pcm_format && + dev->channels == sound->channels && + dev->rate == sound->rate) + return; + + if (dev->pcm_format) + snd_pcm_drop(dev->handle); + + snd_pcm_hw_params_alloca(&hwp); + snd_pcm_hw_params_any(dev->handle, hwp); + snd_pcm_hw_params_set_access(dev->handle, hwp, SND_PCM_ACCESS_RW_INTERLEAVED); + snd_pcm_hw_params_set_format(dev->handle, hwp, sound->pcm_format); + snd_pcm_hw_params_set_channels(dev->handle, hwp, sound->channels); + snd_pcm_hw_params_set_rate(dev->handle, hwp, sound->rate, 0); + snd_pcm_hw_params_set_period_size(dev->handle, hwp, + sound->period_bytes/sound->sample_bytes, 0); + snd_pcm_hw_params_set_buffer_size(dev->handle, hwp, + sound->period_bytes*4/sound->sample_bytes); + snd_pcm_hw_params(dev->handle, hwp); + dev->pcm_format = sound->pcm_format; + dev->channels = sound->channels; + dev->rate = sound->rate; + dev->sample_bytes = sound->sample_bytes; + dev->period_bytes = sound->period_bytes; + if (dev->buf_size < dev->period_bytes) { + free(dev->period_buf); + dev->period_buf = malloc(dev->period_bytes); + dev->buf_size = dev->period_bytes; + } +} + +void load_some(struct dev *dev, struct sound *sound) +{ + int len; + if (!dev || !dev->handle || !sound) + return; + + switch(sound->format) { + case FORMAT_WAVE: + len = read_wave(sound, + dev->period_buf + dev->present, + dev->period_bytes - dev->present); + break; + case FORMAT_VORBIS: + len = read_vorbis(sound, + dev->period_buf + dev->present, + dev->period_bytes - dev->present); + break; + default: + sound->eof = 1; + len = 0; + } + dev->present += len; +} + +struct sound *open_sound(char *dir, char *name, int ino) +{ + char path[200]; + int fd; + struct sound *s; + char *eos, *eos1; + + strcpy(path, dir); + strcat(path, "/"); + strcat(path, name); + fd = open(path, O_RDONLY); + if (fd < 0) + return NULL; + s = malloc(sizeof(*s)); + if (!s) + return NULL; + memset(s, 0, sizeof(*s)); + s->fd = fd; + s->empty = 0; + s->eof = 0; + s->seen = 0; + s->name = strdup(name); + s->ino = ino; + s->posn = 0; + s->bytes = s->bytes_used = 0; + + /* check for millisecond suffix */ + eos = name + strlen(name); + while (eos > name && isdigit(eos[-1])) + eos--; + if (eos > name && eos[-1] == '-' && eos[0]) { + s->posn = atol(eos); + eos--; + } + /* Now pick off scenario name */ + eos1 = eos; + while (eos1 > name && isalpha(eos1[-1])) + eos1--; + if (eos1 > name && eos1 < eos && eos - eos1 < 20) { + strncpy(s->scenario, eos1, eos-eos1); + s->scenario[eos-eos1] = 0; + } + + if (lseek(fd, 0L, 2) == 0) { + close(fd); + s->fd = -1; + s->empty = 1; + s->format = FORMAT_UNKNOWN; + return s; + } + lseek(fd, 0L, 0); + /* Read header and set parameters */ + + if (parse_wave(s)) + s->format = FORMAT_WAVE; + else if (parse_vorbis(s)) + s->format = FORMAT_VORBIS; + else + s->format = FORMAT_UNKNOWN; + + if (s->rate <= 8000) { + /* 100 ms == 800 samples, 1600 bytes */ + s->period_bytes = s->rate / 100 * s->sample_bytes; + } else { + /* 44100, 2 seconds, 4 bytes would be 160K !! + * Doesn't work yet. + */ + s->period_bytes = 8192; + } + + if (s->posn) + switch(s->format) { + case FORMAT_WAVE: + seek_wave(s, s->posn); + break; + case FORMAT_VORBIS: + seek_vorbis(s, s->posn); + break; + } + + return s; + + fail: + close(s->fd); + free(s->name); + free(s); + return NULL; +} + +void close_sound(struct sound *sound) +{ + close(sound->fd); + free(sound->name); + free(sound); +} + + +struct sound *find_match(struct list_head *list, + char *name, int ino, + int *matched) +{ + /* If name/ino is found in list, return it and set + * matched. + * else return previous entry (or NULL) and clear matched. + */ + struct sound *rv = NULL; + struct sound *s; + + *matched = 0; + list_for_each_entry(s, list, list) { + int c = strcmp(s->name, name); + if (c > 0) + /* we have gone beyond */ + break; + rv = s; + if (c == 0) { + if (s->ino == ino) + *matched = 1; + break; + } + } + return rv; +} + +void scan_dir(char *path, struct list_head *soundqueue) +{ + DIR *dir = opendir(path); + struct dirent *de; + struct sound *match; + struct sound *pos; + + list_for_each_entry(match, soundqueue, list) + match->seen = 0; + + while ((de = readdir(dir)) != NULL) { + struct sound *new; + int matched = 0; + if (de->d_ino == 0 || + de->d_name[0] == '.') + continue; + + match = find_match(soundqueue, de->d_name, de->d_ino, &matched); + if (matched) { + match->seen = 1; + continue; + } + new = open_sound(path, de->d_name, de->d_ino); + if (! new) + continue; + new->seen = 1; + if (match) + list_add(&new->list, &match->list); + else + list_add(&new->list, soundqueue); + } + closedir(dir); + + list_for_each_entry_safe(match, pos, soundqueue, list) + if (!match->seen) { + list_del(&match->list); + close_sound(match); + } +} + +void play_buf(struct dev *dev) +{ + if (dev->present == dev->period_bytes) { + alarm(30); + snd_pcm_writei(dev->handle, + dev->period_buf, + dev->period_bytes / dev->sample_bytes); + alarm(0); + dev->present = 0; + } +} + +void dev_close(struct dev *dev) +{ + if (!dev->handle) + return; + if (dev->present) { + memset(dev->period_buf + dev->present, 0, + dev->period_bytes - dev->present); + snd_pcm_writei(dev->handle, + dev->period_buf, + dev->period_bytes / dev->sample_bytes); + dev->present = 0; + } + snd_pcm_drain(dev->handle); + snd_pcm_close(dev->handle); + dev->handle = NULL; +} + +char *dir = "/var/run/sound"; +int dfd; +struct dev dev; +int suspend_handle; +struct sound *last = NULL; +struct event work_ev; + +static void do_scan(int fd, short ev, void *vp) +{ + struct list_head *soundqueue = vp; + struct sound *next; + struct timeval tv = {0, 0}; + + fcntl(dfd, F_NOTIFY, DN_CREATE|DN_DELETE|DN_RENAME); + scan_dir(dir, soundqueue); + + if (list_empty(soundqueue)) { + dev_close(&dev); + set_scenario(&dev, "off"); + suspend_allow(suspend_handle); + last = NULL; + return; + } + next = list_entry(soundqueue->next, + struct sound, list); + if (next->empty) { + dev_close(&dev); + set_scenario(&dev, next->scenario); + suspend_allow(suspend_handle); + last = next; + return; + } + suspend_block(suspend_handle); + event_add(&work_ev, &tv); +} + +static void do_work(int fd, short ev, void *vp) +{ + struct list_head *soundqueue = vp; + struct sound *next; + + if (list_empty(soundqueue)) { + set_scenario(&dev, "off"); + return; + } + next = list_entry(soundqueue->next, + struct sound, list); + if (next->empty) { + set_scenario(&dev, next->scenario); + return; + } + + if (next != last) { + set_params(&dev, next); + last = next; + } + load_some(&dev, next); + play_buf(&dev); + if (next->eof) { + char buf[1000]; + sprintf(buf, "%s/%s", dir, next->name); + unlink(buf); + list_del(&next->list); + close_sound(next); + do_scan(fd, ev, vp); + } else { + struct timeval tv = {0, 0}; + event_add(&work_ev, &tv); + } +} + +static int suspend(void *vp) +{ + /* Don't need to do anything here, just as long as + * we cause suspend to block until check_alarms + * had a chance to run. + */ + do_scan(-1, 0, vp); + return 1; +} + + +int main(int argc, char *argv[]) +{ + struct list_head soundqueue; + struct event ev; + + INIT_LIST_HEAD(&soundqueue); + + suspend_handle = suspend_block(-1); + mkdir(dir, 0755); + dfd = open(dir, O_RDONLY|O_DIRECTORY); + if (dfd < 0) { + fprintf(stderr, "sound: Cannot open %s\n", dir); + exit(1); + } + + event_init(); + + suspend_watch(suspend, NULL, &soundqueue); + signal_set(&ev, SIGIO, do_scan, &soundqueue); + signal_add(&ev, NULL); + fcntl(dfd, F_NOTIFY, DN_CREATE|DN_DELETE|DN_RENAME); + + event_set(&work_ev, -1, 0, do_work, &soundqueue); + + memset(&dev, 0, sizeof(dev)); + + suspend_allow(suspend_handle); + event_loop(0); + exit(0); +} diff --git a/utils/dialer.py b/utils/dialer.py new file mode 100644 index 0000000..0f67243 --- /dev/null +++ b/utils/dialer.py @@ -0,0 +1,334 @@ +#!/usr/bin/env python + +# TODO +# - Show time +# - Make actual call +# - integrate with launcher + +# Dialer +# +# Listen for the 'voice-dial' selection. When it is active we steal +# it and raise the dialer. +# Display is: +# Number, or DTMF send +# Name/number (time-of-call) +# Keypad: +# +# 1 2 3 BS +# 4 5 6 BS +# 7 8 9 Ca +# * 0 # ll +# +# Interaction with Modem: +# - request call-out +# write number to /var/run/gsm-state/call +# - answer incoming +# write 'answer' to /var/run/gsm-state/call +# - hang-up +# write empty string to /var/run/gsm-state/call +# - determine status: on-call, incoming, idle +# examine /var/run/gsm-state/status +# - send request +# - recv reply + +import gtk, pygtk, gobject +import pango +import os, sys, time +import dnotify +from subprocess import Popen +from contactdb import contacts + +def record(key, value): + f = open('/var/run/gsm-state/.new.' + key, 'w') + f.write(value) + f.close() + os.rename('/var/run/gsm-state/.new.' + key, + '/var/run/gsm-state/' + key) + +def recall(key): + try: + fd = open("/var/run/gsm-state/" + key) + l = fd.read(1000) + fd.close() + except IOError: + l = "" + return l.strip() + + +class Dialer(gtk.Window): + def __init__(self): + gtk.Window.__init__(self) + self.set_default_size(480,640) + self.set_title("Dialer") + self.connect('destroy', self.close_win) + + self.number = "" + self.dtmf = False + + self.oncall = False + + self.book = contacts() + self.create_ui() + self.watch_clip('voice-dial') + + d = dnotify.dir('/var/run/gsm-state') + self.status_watcher = d.watch('status', self.check_status) + self.incoming_watcher = d.watch('incoming', self.check_incoming) + self.show() + + def close_win(self, *a): + gtk.main_quit() + + def create_ui(self): + + fd = pango.FontDescription('sans 10') + fd.set_absolute_size(70 * pango.SCALE) + self.bfont = fd + fd = pango.FontDescription('sans 10') + fd.set_absolute_size(80 * pango.SCALE) + self.nfont = fd + self.nfont_size = 80 * pango.SCALE + fd = pango.FontDescription('sans 10') + fd.set_absolute_size(80 * pango.SCALE) + self.cfont = fd + self.cfont_size = 80 * pango.SCALE + v = gtk.VBox(); v.show(); self.add(v) + + # number or DTMF + n = gtk.Entry(); n.show() + n.modify_font(self.nfont) + n.set_alignment(0.5) + n.set_size_request(-1, 90) + v.pack_start(n, expand=False) + self.num = n + + # name (or number) of other end. + n = gtk.Label(); n.show() + n.modify_font(self.nfont) + n.set_size_request(-1, 90) + v.pack_start(n, expand=False) + self.callee = n + self.check_callee_font() + + k = self.create_keypad() + v.pack_start(k, expand=True) + + def create_keypad(self): + h = gtk.HBox(); h.show() + + h.pack_start(self.create_col('1','4','7','*')) + h.pack_start(self.create_col('2','5','8','0')) + h.pack_start(self.create_col('3','6','9','#')) + cl = self.create_col('BS','CA\nLL') + h.pack_start(cl) + ch = cl.get_children() + self.BS = ch[0] + self.CALL = ch[1] + h.set_homogeneous(True) + return h + + def create_col(self, *r): + v = gtk.VBox(); v.show(); v.set_homogeneous(True) + for b in r: + bt = gtk.Button(b); + bt.child.modify_font(self.bfont) + bt.connect('button_press_event', self.press, b) + bt.set_property('can-focus', False) + bt.show() + v.pack_start(bt) + return v + + def press(self, b, ev, key): + if len(key) == 1: + if self.oncall == 2: + # Incoming call needs to be answered + return + if self.oncall and not self.dtmf: + self.num.set_text("") + self.num.set_position(-1) + self.dtmf = True + self.num.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("blue")) + if not self.oncall and key == '#' and self.num.get_text() == "": + key = '+' + self.num.insert_text(key, self.num.get_position()) + self.num.set_position(self.num.get_position()+1) + if self.oncall: + self.do_dtmf(key) + self.check_num_font() + n = self.num.get_text() + if len(n) <= 1: + self.book.load() + e = self.book.find_num(n) + if e: + self.callee.set_text('To:' + e.name) + self.check_callee_font() + else: + self.callee.set_text('') + elif key == 'BS': + if self.oncall: + self.endcall() + else: + p = self.num.get_position() + if p > 0: + self.num.delete_text(p-1, p) + self.num.set_position(p-1) + self.check_num_font() + n = self.num.get_text() + if len(n) <= 1: + self.book.load() + e = self.book.find_num(n) + if e: + self.callee.set_text('To:' + e.name) + self.check_callee_font() + else: + self.callee.set_text('') + else: + if self.oncall == 2: + self.takecall() + elif not self.oncall: + self.makecall() + + def watch_clip(self, board): + self.cb = gtk.Clipboard(selection=board) + self.targets = [ (gtk.gdk.SELECTION_TYPE_STRING, 0, 0) ] + + self.cb.set_with_data(self.targets, self.get, self.got_clip, None) + + def got_clip(self, clipb, data): + a = clipb.wait_for_text() + if not self.oncall: + if a[:9] == "Incoming:": + self.incoming(a[9:]) + else: + self.num.set_text(a) + self.num.set_position(-1) + self.check_num_font() + self.makecall() + self.cb.set_with_data(self.targets, self.get, self.got_clip, None) + self.present() + + def incoming(self, num): + + self.BS.child.set_text("En") + self.CALL.child.set_text("OK") + self.oncall = 2 + self.dtmf = False + self.book.load() + self.set_incoming(num) + + def set_incoming(self, num): + if num == '' or num == '-': + num = "Private Number" + self.num.set_text(num) + self.num.set_position(-1) + self.check_num_font() + self.number = num + e = self.book.find_num(num) + if e: + num = e.name + self.callee.set_text('From:' + num) + self.check_callee_font() + + def check_incoming(self, f): + if self.oncall != 2: + return + n = recall('incoming') + self.set_incoming(n) + + def get(self, sel, info, data): + sel.set_text("Number Please") + + def check_status(self, f): + l = recall('status') + if l == 'INCOMING': + l = recall('incoming') + self.incoming(l) + self.present() + elif l == 'BUSY': + self.endcall() + self.callee.set_text('BUSY') + self.check_callee_font() + self.present() + elif l == 'on-call': + pass + elif l == '': + self.endcall() + + def check_num_font(self): + n = self.num.get_text() + l = len(n) + if l <= 9: + s = 80 * pango.SCALE + else: + if l > 16: + l = 16 + s = 80 * pango.SCALE * 9 / l + if self.nfont_size != s: + self.nfont.set_absolute_size(s) + self.nfont_size = s + self.num.modify_font(self.nfont) + + + def check_callee_font(self): + n = self.callee.get_text() + l = len(n) + if l <= 9: + s = 80 * pango.SCALE + else: + if l > 16: + l = 16 + s = 80 * pango.SCALE * 9 / l + if self.cfont_size != s: + self.cfont.set_absolute_size(s) + self.cfont_size = s + self.callee.modify_font(self.cfont) + + + def makecall(self): + n = self.num.get_text() + self.num.select_region(0,-1) + if not n: + return + self.BS.child.set_text("En") + self.oncall = True + #Popen(['alsactl', '-f', '/usr/share/openmoko/scenarios/gsmhandset.state', + # 'restore' ], shell=False, close_fds = True) + self.number = n + self.dtmf = False + self.book.load() + e = self.book.find_num(n) + if e: + self.callee.set_text('To:' + e.name) + else: + self.callee.set_text('To:' + n) + self.check_callee_font() + record('call',n) + + def takecall(self): + self.oncall = True + #Popen(['alsactl', '-f', '/usr/share/openmoko/scenarios/gsmhandset.state', + # 'restore' ], shell=False, close_fds = True) + self.num.select_region(0,-1) + record('call','answer') + + def endcall(self): + if self.oncall == False: + return + record('call','') + self.oncall = False + #Popen(['alsactl', '-f', '/usr/share/openmoko/scenarios/stereoout.state', + # 'restore' ], shell=False, close_fds = True) + self.BS.child.set_text("BS") + self.CALL.child.set_text("CA\nLL") + self.num.set_text("") + self.num.modify_text(gtk.STATE_NORMAL, gtk.gdk.color_parse("black")) + self.num.set_position(-1) + self.callee.set_text("") + self.check_callee_font() + + def do_dtmf(self, ch): + record('dtmf',ch) + + +o = Dialer() +gtk.main() diff --git a/utils/tapinput-dextr.py b/utils/tapinput-dextr.py new file mode 100644 index 0000000..799be2a --- /dev/null +++ b/utils/tapinput-dextr.py @@ -0,0 +1,145 @@ +#!/usr/bin/env python + +# Copyright (C) 2011-2012 Neil Brown +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +# Connect 'tapboard' with 'fakeinput' to make a working on-screen +# keyboard. +# This script uses a long press on the 'power' button to raise +# the keyboard, and also a double-tap on the 'z' axis of the +# accelerometer. However that requires a kernel driver that I +# never published, so it won't work at the moment. + +import time, gtk +from fakeinput import fakeinput +from tapboard_dextr import TapBoard +from evdev import EvDev +import gobject + +class KeyboardIcon(gtk.StatusIcon): + def __init__(self, activate = None): + gtk.StatusIcon.__init__(self) + self.set_from_file('/usr/local/pixmaps/tapinput-dextr.png') + self.set_activate(activate) + + def set_activate(self, activate): + if activate: + self.connect('activate', activate) + +power_timer = None +def power_pressed(cnt, type, code, value, win): + if type != 1: + # not a key press + return + if code != 116: + # not the power key + return + global power_timer + if value != 1: + # not a down press + if power_timer != None: + gobject.source_remove(power_timer) + power_timer = None + return + power_timer = gobject.timeout_add(300, power_held, win) + +def power_held(win): + global power_timer + power_timer = None + if win.visible: + win.hide() + win.visible = False + else: + win.hide() + win.show() + win.activate() + win.visible = True + return False + +last_tap = 0 +def tap_pressed(cnt, type, code, value, win): + global last_tap + if type != 1: + # not a key press + return + if code != 309: + # not BtnZ + return + if value != 1: + # only want dow, not up + return + now = time.time() + print now, last_tap + if now - last_tap < 0.2: + # two taps + if win.visible: + win.hide() + win.visible = False + else: + win.hide() + win.show() + win.activate() + win.visible = True + last_tap = 0 + else: + last_tap = now + +ico = KeyboardIcon() +w = gtk.Window(type=gtk.WINDOW_POPUP) +w.visible = True +w.connect("destroy", lambda w: gtk.main_quit()) +ti = TapBoard() +w.add(ti) +w.set_default_size(ti.width, ti.height) +root = gtk.gdk.get_default_root_window() +(x,y,width,height,depth) = root.get_geometry() +x = int((width - ti.width)/2) +y = int((height - ti.height)/2) +w.move(x,y) +def activate(ignore): + w.hide() + w.show() + w.visible = True + w.activate() + w.move(x,y) +ico.set_activate(activate) +fi = fakeinput() +def dokey(ti, str): + fi.send_char(str) +ti.connect('key', dokey) +def domove(ti, x,y): + global startx, starty + if x == 0 and y == 0: + (startx, starty) = w.get_position() + x = 0 + w.move(startx+x, starty+y) + +ti.connect('move', domove) +def hideme(ti): + w.hide() + w.visible = False +ti.connect('hideme', hideme) +try: + pbtn = EvDev("/dev/input/power", power_pressed, w) + #tbtn = EvDev("/dev/input/accel", tap_pressed, w) +except: + pass +ti.show() +w.show() +fi.new_window() +hideme(ti) +gtk.main() + diff --git a/utils/tapinput.py b/utils/tapinput.py index d39a79a..139fd88 100755 --- a/utils/tapinput.py +++ b/utils/tapinput.py @@ -134,11 +134,12 @@ def hideme(ti): ti.connect('hideme', hideme) try: pbtn = EvDev("/dev/input/power", power_pressed, w) - tbtn = EvDev("/dev/input/accel", tap_pressed, w) + #tbtn = EvDev("/dev/input/accel", tap_pressed, w) except: pass ti.show() w.show() fi.new_window() +hideme(ti) gtk.main() -- 2.39.5