Skip to content

Commit a39ba7a

Browse files
author
okay
committedDec 25, 2020
[sharenote] add shared note app
1 parent cd3c282 commit a39ba7a

File tree

7 files changed

+26094
-2
lines changed

7 files changed

+26094
-2
lines changed
 

‎src/rmkit/fb/fb.cpy

+1-1
Original file line numberDiff line numberDiff line change
@@ -628,7 +628,7 @@ namespace framebuffer:
628628
int perform_redraw(bool):
629629
#ifndef PERF_BUILD
630630
msync(self.fbmem, self.byte_size, MS_SYNC)
631-
self.draw_circle_filled(last_mouse_ev.x, last_mouse_ev.y, 4, 2, BLACK)
631+
// self.draw_circle_filled(last_mouse_ev.x, last_mouse_ev.y, 4, 2, BLACK)
632632
self.save_png()
633633
#endif
634634
return 0

‎src/rmkit/ui/layouts.cpy

+4
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ namespace ui:
2828
w->show()
2929
self.visible = true
3030

31+
void refresh():
32+
for auto ch : children:
33+
ch->dirty = 1
34+
3135
// Layouts generally don't receive events
3236
bool ignore_event(input::SynMotionEvent &ev):
3337
return true

‎src/rmkit/ui/main_loop.cpy

+1-1
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ namespace ui:
235235
if ev.x == -1 || ev.y == -1:
236236
return false
237237

238-
mouse_down := ev.left || ev.right || ev.middle
238+
mouse_down := ev.left > 0 || ev.right > 0 || ev.middle > 0
239239

240240
widgets := display_scene->widgets;
241241
for auto it = widgets.rbegin(); it != widgets.rend(); it++:

‎src/sharenote/Makefile

+4
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
include ../actions.make
2+
3+
EXE=sharenote
4+
FILES=main.cpy

‎src/sharenote/main.cpy

+330
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
#include <sys/types.h>
2+
#include <sys/socket.h>
3+
#include <netdb.h>
4+
#include <stdio.h>
5+
#include <stdlib.h>
6+
#include <unistd.h>
7+
#include <string.h>
8+
#include <iostream>
9+
#include "../build/rmkit.h"
10+
#include "../vendor/json/json.hpp"
11+
#include "../shared/string.h"
12+
13+
#define BUF_SIZE 1024
14+
15+
// TODO:
16+
// * implement TINIT
17+
// * implement latency calculation (while drawing?)
18+
// * implement room counter
19+
// * implement shared clear
20+
// * implement undrawing when server doesn't respond within time limit
21+
22+
// message types
23+
#define TINIT "init"
24+
#define TJOIN "join"
25+
#define TDRAW "draw"
26+
#define TCLEAR "clear"
27+
28+
using json = nlohmann::json
29+
30+
HOST := getenv("HOST") ? getenv("HOST") : "rmkit.dev"
31+
PORT := getenv("PORT") ? getenv("PORT") : "65432"
32+
33+
using PLS::Observable
34+
class AppState:
35+
public:
36+
Observable<bool> erase
37+
Observable<string> room
38+
AppState STATE
39+
40+
class JSONSocket:
41+
public:
42+
int sockfd
43+
struct addrinfo hints;
44+
struct addrinfo *result, *rp;
45+
char buf[BUF_SIZE]
46+
string leftover
47+
deque<json> out_queue
48+
std::mutex lock
49+
deque<json> in_queue
50+
const char* host
51+
const char* port
52+
bool _connected = false
53+
54+
JSONSocket(const char* host, port):
55+
sockfd = socket(AF_INET, SOCK_STREAM, 0)
56+
memset(&hints, 0, sizeof(struct addrinfo));
57+
hints.ai_family = AF_UNSPEC;
58+
hints.ai_socktype = SOCK_DGRAM;
59+
hints.ai_flags = 0;
60+
hints.ai_protocol = 0;
61+
self.host = host
62+
self.port = port
63+
self.leftover = ""
64+
65+
new thread([=]() {
66+
s := getaddrinfo(host, port, &hints, &result)
67+
if s != 0:
68+
fprintf(stderr, "getaddrinfo: %s\n", gai_strerror(s));
69+
exit(EXIT_FAILURE);
70+
71+
self.listen()
72+
})
73+
74+
new thread([=]() {
75+
self.write_loop();
76+
})
77+
78+
void write_loop():
79+
while true:
80+
if self.sockfd < 3:
81+
debug "CANT WRITE TO SOCKET"
82+
sleep(1)
83+
continue
84+
85+
self.lock.lock()
86+
if !self._connected:
87+
// wait for listen() to reconnect
88+
self.lock.unlock()
89+
sleep(1)
90+
continue
91+
92+
for (i:=0;i<self.in_queue.size();i++):
93+
json_dump := self.in_queue[i].dump()
94+
msg_c_str := json_dump.c_str()
95+
::send(self.sockfd, msg_c_str, strlen(msg_c_str), MSG_DONTWAIT)
96+
::send(self.sockfd, "\n", 1, MSG_DONTWAIT)
97+
self.in_queue.clear()
98+
self.lock.unlock()
99+
100+
void write(json &j):
101+
self.lock.lock()
102+
self.in_queue.push_back(j);
103+
self.lock.unlock()
104+
105+
void listen():
106+
bytes_read := -1
107+
while true:
108+
while bytes_read <= 0:
109+
err := connect(self.sockfd, self.result->ai_addr, self.result->ai_addrlen)
110+
if err == 0 || errno == EISCONN:
111+
debug "(re)connected"
112+
self.lock.lock()
113+
self._connected = true
114+
self.lock.unlock()
115+
break
116+
debug "(re)connecting...", err, errno
117+
self.lock.lock()
118+
close(self.sockfd)
119+
self._connected = false
120+
self.lock.unlock()
121+
sleep(1)
122+
123+
bytes_read = read(sockfd, buf, BUF_SIZE-1)
124+
if bytes_read <= 0:
125+
if bytes_read == -1 and errno == EAGAIN:
126+
continue
127+
128+
close(self.sockfd)
129+
self.sockfd = socket(AF_INET, SOCK_STREAM, 0)
130+
sleep(1)
131+
continue
132+
buf[bytes_read] = 0
133+
sbuf := string(buf)
134+
memset(buf, 0, BUF_SIZE)
135+
136+
msgs := str_utils::split(sbuf, '\n')
137+
if leftover != "" && msgs.size() > 0:
138+
msgs[0] = leftover + msgs[0]
139+
leftover = ""
140+
if sbuf[sbuf.length()-1] != '\n':
141+
leftover = msgs.back()
142+
msgs.pop_back()
143+
for (i:=0; i!=msgs.size(); ++i):
144+
try:
145+
msg_json := json::parse(msgs[i].begin(), msgs[i].end())
146+
lock.lock()
147+
out_queue.push_back(msg_json)
148+
lock.unlock()
149+
catch(...):
150+
debug "COULDNT PARSE", msgs[i]
151+
152+
ui::TaskQueue::wakeup()
153+
154+
155+
class Note: public ui::Widget:
156+
public:
157+
int prevx = -1, prevy = -1
158+
framebuffer::VirtualFB *vfb
159+
bool full_redraw
160+
JSONSocket *socket
161+
162+
Note(int x, y, w, h, JSONSocket* s): Widget(x, y, w, h):
163+
vfb = new framebuffer::VirtualFB(self.fb->width, self.fb->height)
164+
vfb->clear_screen()
165+
self.full_redraw = true
166+
self.socket = s
167+
self.mouse_down = false
168+
169+
void on_mouse_up(input::SynMotionEvent &ev):
170+
prevx = prevy = -1
171+
172+
bool ignore_event(input::SynMotionEvent &ev):
173+
return input::is_touch_event(ev) != NULL
174+
175+
void on_mouse_move(input::SynMotionEvent &ev):
176+
width := STATE.erase ? 20 : 5
177+
if prevx != -1:
178+
vfb->draw_line(prevx, prevy, ev.x, ev.y, width, GRAY)
179+
self.dirty = 1
180+
181+
json j
182+
j["type"] = TDRAW
183+
j["prevx"] = prevx
184+
j["prevy"] = prevy
185+
j["x"] = ev.x
186+
j["y"] = ev.y
187+
j["width"] = width
188+
j["color"] = STATE.erase ? WHITE : BLACK
189+
190+
self.socket->write(j)
191+
192+
prevx = ev.x
193+
prevy = ev.y
194+
195+
void render():
196+
if self.full_redraw:
197+
self.full_redraw = false
198+
memcpy(self.fb->fbmem, vfb->fbmem, vfb->byte_size)
199+
return
200+
201+
dirty_rect := self.vfb->dirty_area
202+
for int i = dirty_rect.y0; i < dirty_rect.y1; i++:
203+
memcpy(&fb->fbmem[i*fb->width + dirty_rect.x0], &vfb->fbmem[i*fb->width + dirty_rect.x0],
204+
(dirty_rect.x1 - dirty_rect.x0) * sizeof(remarkable_color))
205+
self.fb->dirty_area = vfb->dirty_area
206+
self.fb->dirty = 1
207+
framebuffer::reset_dirty(vfb->dirty_area)
208+
209+
210+
class EraseButton: public ui::Button:
211+
public:
212+
EraseButton(int x, y, w, h): Button(x, y, w, h, "erase"):
213+
pass
214+
215+
void on_mouse_down(input::SynMotionEvent &ev):
216+
STATE.erase = !STATE.erase
217+
debug "SETTING ERASER TO", STATE.erase
218+
self.dirty = 1
219+
if STATE.erase:
220+
self.text = "pen"
221+
else:
222+
self.text = "eraser"
223+
224+
class RoomInput: public ui::TextInput:
225+
public:
226+
227+
RoomInput(int x, y, w, h, JSONSocket *sock): TextInput(x, y, w, h, "default"):
228+
self->events.done += PLS_LAMBDA(string &s):
229+
self.join(s)
230+
;
231+
232+
void join(string room):
233+
debug "SETTING ROOM TO", room
234+
STATE.room = room
235+
236+
237+
class App:
238+
public:
239+
Note *note
240+
JSONSocket *socket
241+
ui::HorizontalLayout *button_bar
242+
243+
App():
244+
demo_scene := ui::make_scene()
245+
ui::MainLoop::set_scene(demo_scene)
246+
247+
fb := framebuffer::get()
248+
fb->clear_screen()
249+
fb->redraw_screen()
250+
w, h = fb->get_display_size()
251+
252+
socket = new JSONSocket(HOST, PORT)
253+
note = new Note(0, 0, w, h-50, socket)
254+
demo_scene->add(note)
255+
256+
button_bar = new ui::HorizontalLayout(0, 0, w, 50, demo_scene)
257+
hbar := new ui::VerticalLayout(0, 0, w, h, demo_scene)
258+
hbar->pack_end(button_bar)
259+
260+
erase_button := new EraseButton(0, 0, 200, 50)
261+
room_label := new ui::Text(0, 0, 200, 50, "room: ")
262+
room_label->justify = ui::Text::JUSTIFY::RIGHT
263+
room_button := new RoomInput(0, 0, 200, 50, socket)
264+
265+
button_bar->pack_start(erase_button)
266+
button_bar->pack_end(room_button)
267+
button_bar->pack_end(room_label)
268+
269+
STATE.room(PLS_DELEGATE(self.join_room))
270+
room_button->join("default")
271+
272+
273+
void join_room(string room):
274+
// we are not connected to socket just yet
275+
json j
276+
j["type"] = TJOIN
277+
j["room"] = room
278+
socket->write(j)
279+
// hit URL for downloading room image
280+
url := "http://rmkit.dev:65431/room/" + room
281+
room_file := "/tmp/room_" + room
282+
curl_cmd := "curl " + url + " > " + room_file
283+
ret := system(curl_cmd.c_str())
284+
285+
if ret != 0:
286+
debug "ERROR WITH CURL?"
287+
else:
288+
debug "DISPLAYING IMAGE"
289+
self.note->vfb->load_from_png(room_file)
290+
ui::MainLoop::full_refresh()
291+
292+
def handle_key_event(input::SynKeyEvent ev):
293+
// pressing any button will clear the screen
294+
if ev.key == KEY_LEFT:
295+
debug "CLEARING SCREEN"
296+
note->vfb->clear_screen()
297+
ui::MainLoop::fb->clear_screen()
298+
button_bar->refresh()
299+
300+
def handle_server_response():
301+
socket->lock.lock()
302+
for (i:=0; i < socket->out_queue.size(); i++):
303+
j := socket->out_queue[i]
304+
try:
305+
if j["type"] == TDRAW:
306+
note->vfb->draw_line(j["prevx"], j["prevy"], j["x"], j["y"], j["width"], j["color"])
307+
note->dirty = 1
308+
button_bar->refresh()
309+
else if j["type"] == TCLEAR:
310+
// TODO
311+
pass
312+
else:
313+
debug "unknown message type"
314+
catch(...):
315+
debug "COULDN'T PARSE RESPONSE FROM SERVER", j
316+
socket->out_queue.clear()
317+
socket->lock.unlock()
318+
319+
def run():
320+
ui::MainLoop::key_event += PLS_DELEGATE(self.handle_key_event)
321+
322+
while true:
323+
self.handle_server_response()
324+
ui::MainLoop::main()
325+
ui::MainLoop::redraw()
326+
ui::MainLoop::read_input()
327+
328+
app := App()
329+
int main():
330+
app.run()

‎src/sharenote/server/server.py

+221
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,221 @@
1+
#!/usr/bin/env python3
2+
3+
import io
4+
import os
5+
import json
6+
import socket
7+
import threading
8+
9+
from flask import Flask, send_file
10+
from PIL import Image, ImageDraw
11+
12+
HOST = "0.0.0.0"
13+
PORT = 65432 # Port to listen on (non-privileged ports are > 1023)
14+
15+
WIDTH = 1404
16+
HEIGHT = 1872
17+
18+
# TODO:
19+
# * room messages
20+
# * validate messages
21+
# * init message
22+
# * (MSG, DATA) proto
23+
24+
class Room:
25+
def __init__(self, name="default"):
26+
self.clients = []
27+
self.name = name
28+
self.msgs_file = "/tmp/room_"+name+"_msgs"
29+
self.write_handle = open(self.msgs_file, "wb")
30+
self.image = Image.new("L", (WIDTH, HEIGHT), 255)
31+
self.image_file = "/tmp/room_"+name+"_img"
32+
self.imgdraw = ImageDraw.Draw(self.image)
33+
self.lock = threading.Lock()
34+
35+
if os.path.exists(self.image_file):
36+
self.image = Image.open(self.image_file)
37+
38+
if os.path.exists(self.msgs_file):
39+
with open(self.msgs_file, "rb") as f:
40+
for line in f.readlines():
41+
msg_obj = json.loads(line)
42+
x, y, px, py = (
43+
msg_obj["x"],
44+
msg_obj["y"],
45+
msg_obj["prevx"],
46+
msg_obj["prevy"]
47+
)
48+
self.imgdraw.line(
49+
[(px, py), (x, y)],
50+
fill=msg_obj["color"],
51+
width=msg_obj["width"]
52+
)
53+
54+
55+
def __del__(self):
56+
with self.lock:
57+
self.write_handle.close()
58+
59+
def add(self, client):
60+
with self.lock:
61+
self.clients.append(client)
62+
self.write_handle.flush()
63+
64+
with open(self.msgs_file, 'rb') as f:
65+
client.conn.sendall(f.read())
66+
67+
def remove(self, client):
68+
with self.lock:
69+
print("Removing connection", client.conn.addr)
70+
self.clients.remove(client)
71+
72+
def send(self, msg_str):
73+
with self.lock:
74+
for client in self.clients:
75+
try:
76+
client.conn.sendall(msg_str + b'\n')
77+
except:
78+
pass
79+
80+
# make thread safe?
81+
self.write_handle.write(msg_str + b'\n')
82+
self.write_handle.flush()
83+
84+
def clear(self):
85+
with self.lock:
86+
self.image = Image.new("L", (WIDTH, HEIGHT), 255)
87+
self.clear_log()
88+
89+
self.send(b'{"type":"clear"}')
90+
91+
def clear_log(self):
92+
self.write_handle.truncate()
93+
94+
95+
96+
_rooms = { "default": Room(name="default")}
97+
room_lock = threading.Lock()
98+
99+
100+
class ClientThread(threading.Thread):
101+
def __init__(self, addr, conn):
102+
threading.Thread.__init__(self)
103+
self.conn = conn
104+
self.addr = addr
105+
self.room = None
106+
107+
def __del__(self):
108+
with room_lock:
109+
if self.room in _rooms:
110+
_rooms[self.room].remove(self)
111+
112+
def join(self, room):
113+
print("New connection added: ", self.addr, "to room", room)
114+
with room_lock:
115+
if room not in _rooms:
116+
_rooms[room] = Room(name=room)
117+
_rooms[room].add(self)
118+
self.room = room
119+
# TODO send URL with image download
120+
121+
def handle_message(self, msg_obj):
122+
if msg_obj["type"] == "join":
123+
self.join(msg_obj["room"])
124+
125+
if self.room == None:
126+
return
127+
128+
if msg_obj["type"] == "draw":
129+
x, y, px, py = (
130+
msg_obj["x"],
131+
msg_obj["y"],
132+
msg_obj["prevx"],
133+
msg_obj["prevy"]
134+
)
135+
room = _rooms[self.room]
136+
room.imgdraw.line(
137+
[(px, py), (x, y)],
138+
fill=msg_obj["color"],
139+
width=msg_obj["width"]
140+
)
141+
msg_str = bytes(json.dumps(msg_obj), encoding='utf8')
142+
with room_lock:
143+
_rooms[self.room].send(msg_str)
144+
elif msg_obj["type"] == "clear":
145+
with room_lock:
146+
_rooms[self.room].clear()
147+
148+
def run(self):
149+
# self.csocket.send(bytes("Hi, This is from Server..",'utf-8'))
150+
messages = []
151+
message = b""
152+
while True:
153+
try:
154+
data = self.conn.recv(1024)
155+
except ConnectionResetError:
156+
with room_lock:
157+
_rooms[self.room].remove(self)
158+
return
159+
if not data:
160+
break
161+
chunks = data.split(b"\n")
162+
for chunk in chunks[:-1]:
163+
message += chunk
164+
messages.append(json.loads(message))
165+
message = b""
166+
message += chunks[-1]
167+
if data[-1] == "\n":
168+
messages.append(json.loads(message))
169+
message = b""
170+
171+
for msg_obj in messages:
172+
self.handle_message(msg_obj)
173+
174+
messages = []
175+
self.conn.close()
176+
177+
178+
app = Flask(__name__)
179+
180+
@app.route('/room/<room>')
181+
def get_room_image(room):
182+
s = io.BytesIO()
183+
with room_lock:
184+
if room not in _rooms:
185+
return "room not found"
186+
room = _rooms[room]
187+
188+
with room.lock:
189+
im = room.image
190+
im.save(room.image_file, format="png")
191+
im.save(s, format="png")
192+
room.clear_log()
193+
bytes = s.getvalue()
194+
return bytes
195+
196+
def listen_socket():
197+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
198+
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
199+
s.bind((HOST, PORT))
200+
print("server started")
201+
202+
while True:
203+
s.listen()
204+
conn, addr = s.accept()
205+
newthread = ClientThread(addr, conn)
206+
newthread.start()
207+
208+
def listen_http():
209+
app.run(host="0.0.0.0", port=65431)
210+
211+
def main():
212+
thread_socket = threading.Thread(target=listen_socket)
213+
thread_http = threading.Thread(target=listen_http)
214+
215+
thread_socket.start()
216+
thread_http.start()
217+
218+
thread_socket.join()
219+
thread_http.join()
220+
221+
main()

‎src/vendor/json/json.hpp

+25,533
Large diffs are not rendered by default.

0 commit comments

Comments
 (0)
Please sign in to comment.