Skip to content

Data Model

This document describes the data model used by the blinqr system for managing reminder slots.

blinqr manages exactly 5 reminder slots. Each slot represents an independent daily reminder that can be configured through the mobile app.

The core data structure for a reminder:

interface ReminderSlot {
id: number; // 0-4 (immutable, maps to physical slot)
label: string; // User-defined name
icon: string; // Emoji for visual identification
hour: number; // 0-23 (24-hour format)
minute: number; // 0-59
isEnabled: boolean; // Whether this reminder is active
}
FieldTypeRangeDescription
idnumber0-4Unique identifier matching physical LED/button
labelstring1-20 charsHuman-readable name for the reminder
iconstring1 emojiVisual indicator shown in the app UI
hournumber0-23Hour when reminder should fire (24-hour)
minutenumber0-59Minute when reminder should fire
isEnabledbooleantrue/falseWhether the slot is active
  • id is immutable and assigned at creation
  • label should be non-empty; default to “Reminder {id}” if empty
  • icon should be a single emoji character
  • hour and minute define a single daily trigger time
  • Only isEnabled: true slots will trigger reminders

The app initializes with these default slots:

IDLabelIconTimeEnabled
0Water💧09:00Yes
1Meds💊08:00Yes
2Dog🐶18:00Yes
3Task📌12:00No
4Other14:00No

The device uses an 8-bit mask to control LED states:

Bit: 7 6 5 4 3 2 1 0
[reserved ] [4] [3] [2] [1] [0]
│ │ │ │ └── Slot 0 LED
│ │ │ └────── Slot 1 LED
│ │ └────────── Slot 2 LED
│ └────────────── Slot 3 LED
└────────────────── Slot 4 LED
// Set a bit (turn LED on)
mask = mask | (1 << slotId);
// Clear a bit (turn LED off)
mask = mask & ~(1 << slotId);
// Check if bit is set
isSet = (mask & (1 << slotId)) !== 0;
// Convert mask to array of active slot IDs
function maskToSlotIds(mask: number): number[] {
const ids: number[] = [];
for (let i = 0; i < 5; i++) {
if ((mask & (1 << i)) !== 0) {
ids.push(i);
}
}
return ids;
}

User configures slots in the app:

  • Set label, icon, time, enabled status
  • Changes are stored locally on the phone

The app’s reminder engine:

  • Runs a timer checking current time against slot schedules
  • When currentTime === slot.time && slot.isEnabled:
    • Set the corresponding bit in the mask
    • Send updated mask to device via BLE

While a reminder is active:

  • The LED on the device is illuminated
  • The app shows the slot as “ACTIVE”
  • The mask bit remains set

When the user presses the button:

  • Device sends button_events notification with slot index
  • App receives the notification and:
    • Clears the corresponding mask bit
    • Sends updated mask to device
    • Marks slot as “acknowledged today”

At midnight (or configurable time):

  • All “acknowledged today” flags are cleared
  • Reminders can fire again the next day
┌─────────────────┐
│ INACTIVE │
│ (LED off) │
└────────┬────────┘
[Time matches schedule]
┌─────────────────┐
│ ACTIVE │
│ (LED on) │
└────────┬────────┘
[Button pressed]
┌─────────────────┐
│ ACKNOWLEDGED │
│ (LED off) │
└────────┬────────┘
[New day / Reset]
┌─────────────────┐
│ INACTIVE │
│ (Ready again) │
└─────────────────┘
  • Slot configuration stored in memory only
  • Resets to defaults on app restart
  • Persist to AsyncStorage (React Native)
  • Sync across devices via cloud (optional)
  • Export/import configurations
interface ReminderState {
// State
slots: ReminderSlot[];
currentMask: number;
acknowledgedToday: Set<number>;
// Actions
updateSlot: (id: number, updates: Partial<ReminderSlot>) => void;
setMaskBit: (slotId: number) => void;
clearMaskBit: (slotId: number) => void;
setMask: (mask: number) => void;
acknowledgeSlot: (slotId: number) => void;
resetAcknowledgments: () => void;
shouldSlotFire: (slotId: number, hour: number, minute: number) => boolean;
}

When updating a slot, validate:

function validateSlot(slot: Partial<ReminderSlot>): boolean {
if (slot.label !== undefined) {
if (typeof slot.label !== 'string' || slot.label.length > 20) {
return false;
}
}
if (slot.hour !== undefined) {
if (!Number.isInteger(slot.hour) || slot.hour < 0 || slot.hour > 23) {
return false;
}
}
if (slot.minute !== undefined) {
if (!Number.isInteger(slot.minute) || slot.minute < 0 || slot.minute > 59) {
return false;
}
}
return true;
}