-
Notifications
You must be signed in to change notification settings - Fork 22
[WIP] Ofono bridge #83
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: devel
Are you sure you want to change the base?
Changes from all commits
8b385ae
e1a63fb
f43ea67
1994095
0413ddf
ff3d603
b5d29b0
67c2baa
0966d0e
12d27f9
41ff44c
e1f101b
f085e7b
2a084a6
08dc9e8
da2fb47
1650127
0cf96f2
8e50fd7
01e72d3
1b5c9e5
248a30a
54d548f
49a1ee1
becaef5
85e74ce
2a40a20
47350dc
917f7f6
44d6f44
b04f185
9887af1
85cf72c
377c146
1d29e7f
f92f021
378593e
c787dc3
ec17696
886a243
801fb09
3a3500b
65e450e
bb9e456
5af4c3c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,2 @@ | ||
| KERNEL=="ttyAMA0", ENV{OFONO_DRIVER}="sim900" | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,9 @@ | ||
| from helpers import setup_logger | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wait, why? I think you need to revert this particular change, which reverses our "setup_logger" integration work
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. (as in, revert the logger-> logging replacements, not the from_string addition) |
||
| logger = setup_logger(__name__, "warning") | ||
|
|
||
| import vobject | ||
|
|
||
| from address_book import Contact | ||
| from helpers import setup_logger | ||
|
|
||
| logger = setup_logger(__name__, "info") | ||
|
|
||
|
|
||
| class VCardContactConverter(object): | ||
|
|
@@ -52,3 +52,9 @@ def from_vcards(contact_card_files): | |
| contacts += VCardContactConverter.parse_vcard_file(file_path) | ||
| logger.info("finished : {} contacts loaded", len(contacts)) | ||
| return [VCardContactConverter.to_zpui_contact(c) for c in contacts] | ||
|
|
||
| @classmethod | ||
| def from_string(cls, vcard_string): | ||
| # type: (str) -> list | ||
| # Returns a list of ZPUI contacts from a string in vcard format | ||
| return [c for c in vobject.readComponents(vcard_string, ignoreUnreadable=True)] | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,55 @@ | ||
| # This function allows to insert polygons as a list of points whose code is | ||
| # laid out in a shape somewhat similar to the polygon's shape. The problem | ||
| # with that is - order of points in the polygon list matters when drawing it, | ||
| # so we have to reorder the points - otherwise they'll become an untangled mess. | ||
| # So, that's what this function does - untangles the polygons according to | ||
| # a given mapping. | ||
|
|
||
| def untangle_points_by_mapping(points, mapping): | ||
| new_points = [p for p in points] | ||
| for dest, so in enumerate(mapping): | ||
| so = int(so) | ||
| new_points[dest] = points[so] | ||
| return new_points | ||
|
|
||
| # Graphics | ||
|
|
||
| arrow = ( # indices: | ||
| (3, 0), # 0 | ||
| (0, 3),(2, 3),(4, 3),(6, 3), # 1 2 3 4 | ||
| (2, 8),(4, 8) # 5 6 | ||
| ) | ||
|
|
||
| mapping = "0436521" | ||
|
|
||
| arrow = untangle_points_by_mapping(arrow, mapping) | ||
|
|
||
| cross = ( # indices: | ||
| (1, 0), (7, 0), # 0 1 | ||
| (0, 1), (8, 1), # 2 3 | ||
| (4, 3), # 4 | ||
| (3, 4), (5, 4), # 5 6 | ||
| (4, 5), # 7 | ||
| (0, 7), (8, 7), # 8 9 | ||
| (1, 8), (7, 8) # 10 11 | ||
| ) | ||
|
|
||
| mapping = (0, 4, 1, 3, 6, 9, 11, 7, 10, 8, 5, 2) | ||
|
|
||
| cross = untangle_points_by_mapping(cross, mapping) | ||
|
|
||
| phone_handset = ( | ||
| (4, 0), # 0 | ||
| (8, 1), # 1 | ||
| (4, 3), # 2 | ||
| (7, 4), # 3 | ||
|
|
||
| (2, 11), # 4 | ||
| (4, 12), # 5 | ||
| (0, 13), # 6 | ||
| (3, 15) # 7 | ||
| ) | ||
|
|
||
| mapping = "01324576" | ||
|
|
||
| phone_handset = untangle_points_by_mapping(phone_handset, mapping) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,137 +1,171 @@ | ||
|
|
||
|
|
||
| from helpers import setup_logger | ||
|
|
||
| menu_name = "Phone" | ||
|
|
||
| from subprocess import call as os_call | ||
| from time import sleep | ||
| import traceback | ||
|
|
||
| from ui import Refresher, Menu, Printer, PrettyPrinter, DialogBox | ||
| from ui.experimental import NumberKeypadInputLayer | ||
| from helpers import BackgroundRunner, ExitHelper | ||
|
|
||
| from phone import Phone, Modem, ATError | ||
|
|
||
|
|
||
| logger = setup_logger(__name__, "warning") | ||
|
|
||
| i = None | ||
| o = None | ||
| init = None | ||
| phone = None | ||
|
|
||
| def answer(): | ||
| #No context switching as for now, so all we can do is | ||
| #answer call is there's an active one | ||
| try: | ||
| phone.answer() | ||
| except ATError: | ||
| pass | ||
|
|
||
| def hangup(): | ||
| try: | ||
| phone.hangup() | ||
| except ATError: | ||
| from apps import ZeroApp | ||
| from ui import Refresher, NumpadNumberInput, Canvas, MockOutput, FunctionOverlay, \ | ||
| offset_points, get_bounds_for_points, expand_coords, multiply_points, check_coordinates, \ | ||
| convert_flat_list_into_pairs as cflip | ||
| from ui.base_ui import BaseUIElement | ||
| from ui.base_view_ui import BaseView | ||
|
|
||
| import graphics | ||
| from views import InputScreenView | ||
|
|
||
|
|
||
| class InputScreen(NumpadNumberInput): | ||
|
|
||
| message = "Input number:" | ||
| default_pixel_view = "InputScreenView" | ||
| def __init__(self, i, o, *args, **kwargs): | ||
| kwargs["message"] = self.message | ||
| kwargs["value"] = "00000000000000000000000000000000000000000000000000000000012345" | ||
| NumpadNumberInput.__init__(self, i, o, *args, **kwargs) | ||
|
|
||
| def generate_views_dict(self): | ||
| d = NumpadNumberInput.generate_views_dict(self) | ||
| d.update({"InputScreenView":InputScreenView}) | ||
| return d | ||
|
|
||
|
|
||
| class StatusScreen(Refresher): | ||
|
|
||
| arrow_x = 15 | ||
| arrow_y = 40 | ||
| handset_x = 2 | ||
| handset_y = 10 | ||
| arrow_offset = 2 | ||
| counter = 0 | ||
| number_frame = (10, 45, "-10", "-1") | ||
| number_height = 16 | ||
| number_font = ("Fixedsys62.ttf", number_height) | ||
|
|
||
| def __init__(self, *args, **kwargs): | ||
| Refresher.__init__(self, self.show_status, *args, **kwargs) | ||
| self.c = Canvas(self.o) | ||
| self.prepare_number() | ||
| self.predraw_arrow() | ||
| self.predraw_cross() | ||
| self.predraw_handset() | ||
| self.status = {"number":"25250034", "accepted":True, "time":385} | ||
|
|
||
| def prepare_number(self): | ||
| frame_coords = check_coordinates(self.c, self.number_frame) | ||
| nw, nh = get_bounds_for_points(cflip(frame_coords)) | ||
| self.number_c = Canvas(MockOutput(width=nw, height=nh, device_mode=self.o.device_mode)) | ||
|
|
||
| def predraw_arrow(self): | ||
| aw, ah = get_bounds_for_points(graphics.arrow) | ||
| arrow_c = Canvas(MockOutput(width=aw, height=ah, device_mode=self.o.device_mode)) | ||
| arrow_c.polygon(graphics.arrow, fill="white") | ||
| self.arrow_img = arrow_c.get_image() | ||
|
|
||
| def predraw_cross(self): | ||
| aw, ah = get_bounds_for_points(graphics.cross) | ||
| cross_c = Canvas(MockOutput(width=aw, height=ah, device_mode=self.o.device_mode)) | ||
| cross_c.polygon(graphics.cross, fill="white") | ||
| self.cross_img = cross_c.get_image() | ||
|
|
||
| def predraw_handset(self): | ||
| handset = multiply_points(graphics.phone_handset, 2) | ||
| aw, ah = get_bounds_for_points(handset) | ||
| handset_c = Canvas(MockOutput(width=aw, height=ah, device_mode=self.o.device_mode)) | ||
| handset_c.polygon(handset, fill="white") | ||
| self.handset_img = handset_c.get_image() | ||
|
|
||
| def draw_arrow(self, c, flipped=False): | ||
| coords = (self.arrow_x, self.arrow_y, self.arrow_x+self.arrow_img.width, self.arrow_y+self.arrow_img.height) | ||
| clear_coords = expand_coords(coords, self.arrow_offset) | ||
| c.clear(clear_coords) | ||
| img = self.arrow_img if not flipped else self.arrow_img.rotate(180) | ||
| c.image.paste(img, (self.arrow_x, self.arrow_y)) | ||
|
|
||
| def draw_cross(self, c, flipped=False): | ||
| coords = (self.arrow_x, self.arrow_y, self.arrow_x+self.cross_img.width, self.arrow_y+self.cross_img.height) | ||
| clear_coords = expand_coords(coords, self.arrow_offset) | ||
| c.clear(clear_coords) | ||
| c.image.paste(self.cross_img, (self.arrow_x, self.arrow_y)) | ||
|
|
||
| def draw_handset(self, c, flipped=False): | ||
| coords = (self.handset_x, self.handset_y, self.handset_x+self.handset_img.width, self.handset_y+self.handset_img.height) | ||
| c.image.paste(self.handset_img, (self.handset_x, self.handset_y)) | ||
|
|
||
| def get_status(self): | ||
| states = ["incoming", "outgoing", "missed"] | ||
| self.status["state"] = states[self.counter] | ||
| self.counter += 1 | ||
| if self.counter == 3: self.counter = 0 | ||
| return self.status | ||
|
|
||
| def draw_state(self, c, status): | ||
| if status["state"] == "incoming": | ||
| self.draw_arrow(c, flipped=True) | ||
| elif status["state"] == "outgoing": | ||
| self.draw_arrow(c) | ||
| elif status["state"] == "missed": | ||
| self.draw_cross(c) | ||
|
|
||
| def draw_text_state(self, c, status): | ||
| if not status["accepted"]: | ||
| self.c.text(status["state"].capitalize(), (30, 9), font=(self.number_font[0], 16)) | ||
| else: | ||
| status["time"] = status["time"] + 1 | ||
| time_str = "{:02d}:{:02d}".format(*divmod(status["time"], 60)) | ||
| self.c.text(time_str, (30, 9), font=(self.number_font[0], 16)) | ||
|
|
||
| def draw_number(self, c, status): | ||
| self.number_c.clear() | ||
| self.number_c.centered_text(status["number"], font=self.number_font) | ||
| c.image.paste(self.number_c.image, self.number_frame[:2]) | ||
| c.rectangle(self.number_frame) | ||
|
|
||
| def show_status(self): | ||
| self.c.clear() | ||
| status = self.get_status() | ||
| self.draw_number(self.c, status) | ||
| self.draw_handset(self.c) | ||
| self.draw_state(self.c, status) | ||
| self.draw_text_state(self.c, status) | ||
| return self.c.get_image() | ||
|
|
||
|
|
||
| class PhoneApp(ZeroApp): | ||
|
|
||
| menu_name = "Phone" | ||
|
|
||
| def __init__(self, *args, **kwargs): | ||
| ZeroApp.__init__(self, *args, **kwargs) | ||
| self.input_screen = InputScreen(self.i, self.o) | ||
| self.insc_overlay = FunctionOverlay({"KEY_F1":"deactivate", "KEY_F2":"backspace", "KEY_ENTER":self.insc_options}, labels=["Cancel", "Menu", "Bckspc"]) | ||
| self.insc_overlay.apply_to(self.input_screen) | ||
| self.status_screen = StatusScreen(self.i, self.o, keymap={"KEY_ANSWER":self.accept_call, "KEY_HANGUP":self.reject_call}) | ||
|
|
||
| def insc_options(self): | ||
| self.switch_to_status_screen() | ||
|
|
||
| def get_context(self, c): | ||
| self.context = c | ||
|
|
||
| def switch_to_input_screen(self, digit=None): | ||
| self.input_screen.activate() | ||
|
|
||
| def switch_to_status_screen(self, digit=None): | ||
| self.status_screen.activate() | ||
|
|
||
| def accept_call(self): | ||
| self.status_screen.status["accepted"] = True | ||
|
|
||
| def reject_call(self): | ||
| self.status_screen.status["accepted"] = False | ||
|
|
||
| def on_call(self): | ||
| self.c.request_switch() | ||
| self.show_call_status() | ||
|
|
||
| def get_call_status(self): | ||
| raise NotImplementedError | ||
|
|
||
| def show_call_status(self): | ||
| status = self.get_call_status() | ||
| pass | ||
|
|
||
| def phone_status(): | ||
| data = [] | ||
| status = phone.get_status() | ||
| callerid = {} | ||
| if status["state"] != "idle": | ||
| callerid = phone.get_caller_id() | ||
| for key, value in status.iteritems(): | ||
| if value: | ||
| data.append("{}: {}".format(key, value)) | ||
| for key, value in callerid.iteritems(): | ||
| data.append("{}: {}".format(key, value)) | ||
| return data | ||
|
|
||
| def call(number): | ||
| Printer("Calling {}".format(number), i, o, 0) | ||
| try: | ||
| phone.call(number) | ||
| except ATError as e: | ||
| PrettyPrinter("Calling fail! "+repr(e), i, o, 0) | ||
| logger.error("Function stopped executing") | ||
|
|
||
| def call_view(): | ||
| keymap = {"KEY_ANSWER":[call, "value"]} | ||
| NumberKeypadInputLayer(i, o, "Call number", keymap, name="Phone call layer").activate() | ||
|
|
||
| def status_refresher(): | ||
| Refresher(phone_status, i, o).activate() | ||
|
|
||
|
|
||
| def init_hardware(): | ||
| try: | ||
| global phone | ||
| phone = Phone() | ||
| modem = Modem() | ||
| phone.attach_modem(modem) | ||
| except: | ||
| deinit_hardware() | ||
| raise | ||
|
|
||
| def deinit_hardware(): | ||
| phone.detach_modem() | ||
|
|
||
| def wait_for_connection(): | ||
| eh = ExitHelper(i).start() | ||
| while eh.do_run() and init.running and not init.failed: | ||
| sleep(1) | ||
| eh.stop() | ||
|
|
||
| def check_modem_connection(): | ||
| if init.running: | ||
| return False | ||
| elif init.finished: | ||
| return True | ||
| elif init.failed: | ||
| raise Exception("Modem connection failed!") | ||
| else: | ||
| raise Exception("Phone app init runner is in invalid state! (never ran?)") | ||
|
|
||
| def init_app(input, output): | ||
| global i, o, init | ||
| i = input; o = output | ||
| i.set_maskable_callback("KEY_ANSWER", answer) | ||
| i.set_nonmaskable_callback("KEY_HANGUP", hangup) | ||
| try: | ||
| #This not good enough - either make the systemctl library system-wide or add more checks | ||
| os_call(["systemctl", "stop", "serial-getty@ttyAMA0.service"]) | ||
| except Exception as e: | ||
| logger.exception(e) | ||
| init = BackgroundRunner(init_hardware) | ||
| init.run() | ||
|
|
||
| def offer_retry(counter): | ||
| do_reactivate = DialogBox("ync", i, o, message="Retry?").activate() | ||
| if do_reactivate: | ||
| PrettyPrinter("Connecting, try {}...".format(counter), i, o, 0) | ||
| init.reset() | ||
| init.run() | ||
| wait_for_connection() | ||
| callback(counter) | ||
|
|
||
| def callback(counter=0): | ||
| try: | ||
| counter += 1 | ||
| status = check_modem_connection() | ||
| except: | ||
| if counter < 3: | ||
| PrettyPrinter("Modem connection failed =(", i, o) | ||
| offer_retry(counter) | ||
| else: | ||
| PrettyPrinter("Modem connection failed 3 times", i, o, 1) | ||
| else: | ||
| if not status: | ||
| PrettyPrinter("Connecting...", None, o, 0) | ||
| wait_for_connection() | ||
| callback(counter) | ||
| else: | ||
| contents = [["Status", status_refresher], | ||
| ["Call", call_view]] | ||
| Menu(contents, i, o).activate() | ||
| def on_start(self): | ||
| self.switch_to_input_screen() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We'll be shipping this file in our own Debian package, most likely