Dosync reads a handwritten or printed prescription from a photo, works out when each medicine should be taken based on your own daily meal/sleep schedule, and automatically dispenses doses at the right time using an Arduino-driven servo system.
The pipeline has four stages, wired together by a small local Flask server:
-
Daily Rhythm (
meal-timing.html) - a web page where you enter your breakfast, lunch, dinner, and bed times. Submitting the form POSTs the schedule to the local server and kicks off the rest of the pipeline. -
Schedule bridge (
schedule_server.py/schedule_client.py) - a Flask server that stores the schedule you submitted and serves it back to the rest of the pipeline. It also tracks pipeline status (idle, running inference, monitoring, error) so the web page can show live progress. -
Prescription reading (
inference.py) - captures/loads a prescription image, runs it through a local PaddleOCR-VL model to extract raw text, then runs that text through a local Qwen3-VL model to identify each medicine, its frequency, and when each dose should be taken relative to your meals (for example, "30 minutes after lunch"). The actual clock time for each dose is then calculated in plain Python using your real schedule, not by the language model, so the result always matches what you entered on the Daily Rhythm page. The result is saved tooutput.json. -
Dose reminders (
reminder_system.py) - readsoutput.json, builds a daily dose table, and runs indefinitely, sending single-character commands over a serial connection to an Arduino Uno Q at each scheduled time. The Arduino swings the matching servo to dispense the dose, then returns it to rest.
- Python 3.11 (or compatible)
llama-server.exefrom llama.cpp, with the following local models:- PaddleOCR-VL-1.6-GGUF (plus its mmproj file)
- Qwen3-VL-2B-Instruct-UD-Q4_K_XL
- Python packages:
openai,opencv-python,flask,flask-cors,pyserial - An Arduino Uno Q (or compatible board) running a sketch that listens for the single-character servo commands described below
- A webcam or a saved prescription image
Install the Python dependencies:
pip install openai opencv-python flask flask-cors pyserial
-
Update the model and server paths in
inference.py(SERVER_DIR,SERVER_PATH, and the-m/--mmprojpaths inPADDLE_CMDandQWEN_CMD) to match where llama.cpp and the GGUF models are installed on your machine. -
Update
capture_encode_pic()ininference.pyto either capture from your webcam or point at the prescription image you want to process. -
In
reminder_system.py, setSERIAL_PORTto the COM port (Windows) or device path (Linux/Mac) your Arduino is connected to. Runlist_serial_ports()from that file if you are not sure which port to use. -
In
reminder_system.py, updateMEDICINE_SERVO_MAPso the medicine names you expect map to the correct servo channel. Matching is case-insensitive and uses substring matching, so the medicine name extracted from the prescription only needs to contain (or be contained in) one of the keys you list here. Any medicine that does not match one of these keys is skipped, since there is no servo assigned to it.
-
Start the pipeline:
python main.pyThis starts
schedule_server.pyonhttp://127.0.0.1:5500and keeps it running. Leave this terminal window open for the entire session; closing it stops the server and everything downstream of it. -
Open
meal-timing.htmlin your browser. -
Fill in your breakfast, lunch, dinner, and bed times.
-
Click "Start Dosync". This saves your schedule and automatically runs:
inference.py(captures and reads the prescription, savesoutput.json)reminder_system.py(runs indefinitely, dispensing doses on schedule)
The page polls
/pipeline-statusand shows live progress: reading the prescription, then "Dosync is live" once the reminder daemon is monitoring doses.
reminder_system.py only ever sends eight single ASCII characters over
serial, one pair per servo channel:
| Character | Meaning |
|---|---|
a |
Swing servo 1 to dispense position |
A |
Return servo 1 to rest position |
b |
Swing servo 2 to dispense position |
B |
Return servo 2 to rest position |
c |
Swing servo 3 to dispense position |
C |
Return servo 3 to rest position |
d |
Swing servo 4 to dispense position |
D |
Return servo 4 to rest position |
All servo motion, angles, and timing beyond the hold duration are handled on the Arduino side.
| File | Role |
|---|---|
main.py |
Entry point; starts the schedule bridge server and keeps it alive |
meal-timing.html |
Daily Rhythm web page for entering your schedule |
schedule_server.py |
Flask server: stores the schedule, tracks pipeline status, launches inference and reminder stages |
schedule_client.py |
Helper used by inference.py to fetch the current schedule from the server |
inference.py |
OCR + medicine/timing extraction; writes output.json |
reminder_system.py |
Reads output.json, schedules doses, drives the Arduino over serial |
output.json is a JSON array of medicine entries, for example:
[
{
"medicine": "Paracetamol",
"frequency": 2,
"timing": ["08:00", "21:00"]
},
{
"medicine": "Azithromycin",
"frequency": 1,
"timing": ["08:00"],
"note": "500mg strength"
}
]timing entries are always in 24-hour HH:MM format, computed from your
submitted schedule rather than guessed by the language model.
-
Schedule does not seem to be used: check the terminal running
schedule_server.py. It prints the schedule it received and, later, the scheduleinference.pyfetched and the final timing computed for each medicine. If those do not match what you entered, confirm you are running the latestinference.pyand that the server was not left over from a previous run. -
Servo never fires for a known medicine: check the terminal running
reminder_system.pyfor[warn] '<medicine>' isn't one of the 4 known medicinesmessages. This means the extracted medicine name did not match any key inMEDICINE_SERVO_MAP; update the map to include it. -
"Could not reach server" in the browser: make sure
main.pyis still running and that nothing else is bound to port 5500. -
Pipeline stuck on "Schedule saved, but Dosync is already monitoring doses": only one pipeline run is allowed per server session. Stop and restart
main.pyto run the pipeline again with a new schedule.
schedule_server.pyruns Flask's built-in development server, which is fine for local, single-user use but is not intended for production deployment.- The reminder scheduler checks the clock every 20 seconds by default
(
CHECK_INTERVAL_SECONDSinreminder_system.py); doses are only fired once per scheduled minute per day.