On discrete 4-LED Wall Units, the 4 LEDs reflect the Active Call State / Functional Role of the station:
Microcontroller Execution: When any button is pressed, the firmware looks up that button's configured Action Bitmask (Regs 40011â40020, 40031â40038) and Priority Rank (Regs 40061â40076). If configured as Priority 1 (Code Blue), pressing that button activates the Code Blue LED. If configured as Priority 2..4, it activates the Shared Call LED.
Call clearing is implemented as Autonomous Local Clear (0ms Latency) + SBC State Synchronization:
0x0001 CLEAR_ALL) is pressed on the wall unit, the MCU immediately turns OFF Call/Code Blue LEDs and stops the buzzer locally (0ms lag).FC 06 Write Reg 40010 = 0x0001 (Remote Clear Command) to the wall unit, and the MCU turns off its LEDs and mutes buzzer.Corridor Door Lights share the same zero-collision discovery architecture as wall units (see index.html Section 7 and registers.html Section 8):
255 (0x00FF) with Reg 40254 = 0.Reg 40255 = 0x00AA / 0x00FF).Reg 40253 == 1, writes assigned Slave ID via Register 255 (40256), dome echoes reply, burns to 8-byte EEPROM, illuminates Solid Green for 3.0s, and Gateway configures Cycle Interval (40011) and Idle Glow Color (40012).
Each button press event is configured with an Action Bitmask (value = ∑ (1 << code)) stored in EEPROM. Valid action range is 0x00 to 0x7F (bits 0â6). Writes above 0x7F are rejected with Modbus Exception 03 (Illegal Data Value). The following combinations are invalid and must be blocked by the configuring tool: CLEAR_ALL + call bits, ACKNOWLEDGE + call bits, and CALL_SOLID + CALL_BLINK. Should such a value already be present in EEPROM from an earlier configuration, firmware resolves it deterministically: CALL_SOLID + CALL_BLINK renders BLINK; CLEAR_ALL + call bits performs the clear and ignores the call bits; ACKNOWLEDGE + call bits registers the call and ignores the acknowledge.
| Action Bitmask | Action Name | Hardware LED Action | Buzzer Behavior |
|---|---|---|---|
| 0x0000 (0) | DISABLED / NO_ACTION | Button press ignored; no call registered, no LED change. | No buzzer activation. |
| 0x0001 (1) | CLEAR_ALL (Reset Action) | Turns OFF all call LEDs, turns OFF ACK LED, returns to idle. | Mutes / Stops buzzer completely. |
| 0x0002 (2) | CALL_SOLID (Standard Call) | Turns ON assigned button LED Solid with configured color. | Standard periodic beeper (500ms ON / 500ms OFF). |
| 0x0004 (4) | ACKNOWLEDGE (Care Started) | Turns ON ACK (Hold) LED Solid; active call stays solid. | Instantly mutes call buzzer. |
| 0x0008 (8) | MUTE_BUZZER (Snooze) | Leaves active LEDs illuminated as-is; sets muted = true on active call. |
Mutes local buzzer sound only; visual call LEDs remain active. Resets on new higher call or clear. |
| 0x0010 (16) | STAFF_CHECKIN (Attendance) | Turns ON confirmation LED for 3 seconds, then auto-clears. | 1 short 100ms chirp confirmation beep. |
| 0x0020 (32) | CALL_BLINK (Urgent / Blink) | Blinks assigned button LED continuously with configured color. | Continuous emergency alarm sound. |
| 0x0040 (64) | MULTI_CALL_CYCLE (Modifier) | Modifier bit (combine with 0x0002 or 0x0020; not a standalone action). Alternates active call colors in sequence (Bed 1 → Bed 2 → Bed 1). |
Periodic beeper sound. |
CALL_SOLID (2) + MUTE_BUZZER (8) = 10 (0x000A) → Lights visual LED without sounding room buzzer.CALL_BLINK (32) + MULTI_CALL_CYCLE (64) = 96 (0x0060) → Rapid flashing call joins room alternating cycle.ACKNOWLEDGE (4) + STAFF_CHECKIN (16) = 20 (0x0014) → Acknowledges call and logs nurse attendance.CLEAR_ALL (1) + MUTE_BUZZER (8) = 9 (0x0009) → 0ms immediate cancellation and buzzer silence.Polled continuously by the Gateway with 100ms serial timeout:
| Register | Bit / Offset | Parameter Name | Description / Trigger Condition |
|---|---|---|---|
| 40001 | Bit 0..7 | BTN1_SP..BTN4_LP |
Button 1â4 Single / Long Press latched event bits. |
| 40001 | Bit 8..9 | TOILET_BTN1_SP/LP |
Attached Toilet Pull Cord Single Pull / Long Pull (>2s). |
| 40001 | Bit 10..11 | TOILET_BTN2_SP/LP |
Attached Toilet Cancel / Reset Key Single Press / Long Press. |
| 40001 | Bit 12..13 | REMOTE_BTN1_SP/LP |
Attached Remote Handset Call Single / Long Press. |
| 40001 | Bit 14..15 | REMOTE_BTN2_SP/LP |
Attached Remote Handset Cancel Single / Long Press. |
| 40008 | Bit 0..1 | STATUS_CONTINUOUS |
Bit 0 = Bed Occupancy Sensor (1=Occupied, 0=Vacant), Bit 1 = Tamper Switch. (NOT clear-on-read). |
| 40009 | 16-bit | CLEAR_MASK |
Gateway writes handled bits mask to clear processed bits in 40001. Reading returns 0x0000. |
| 40010 | 16-bit | REMOTE_CMD |
Nurse Station downlink & diagnostic command (0x0001 Clear, 0x0004 Ack, 0x0008 Mute, 0x0100 LED Test, 0x0200 Buzzer Chirp Test, 0x0400 EEPROM Check; diagnostic bits 0x0100/0x0200/0x0400 can be combined). |
| PLC Address | Hex | Dec | Access | Storage | Parameter Name | Default | Value Range / Description |
|---|---|---|---|---|---|---|---|
| 40002 | 0x0001 |
1 | R/W | EEPROM | Single-Press Enable Mask | 0xFFFF |
16-bit mask (indexes even bits: 0, 2, 4, 6, 8, 10, 12, 14 in 40001) |
| 40003 | 0x0002 |
2 | R/W | EEPROM | Long-Press Enable Mask | 0xFFFF |
16-bit mask (indexes odd bits: 1, 3, 5, 7, 9, 11, 13, 15 in 40001) |
| 40004 | 0x0003 |
3 | R/W | EEPROM | Long-Press Hold Threshold (ms) | 2000 |
ms required to hold for Long Press (500â5000ms) |
| 40005 | 0x0004 |
4 | R/W | EEPROM | Button Debounce Filter (ms) | 50 |
Hardware debounce filter duration in ms (10â200ms) |
| 40006 | 0x0005 |
5 | R/W | EEPROM | Idle Glow Color | 3 (Blue) |
Idle standby color (3=Blue, 0=OFF) |
| 40011..20, 31..38 | 0x000A.. |
10.. | R/W | EEPROM | Button Action Bitmasks | Bitmasks | Bitmask values: 0x0001 Clear to 0x0040 Multi-Cycle (Range: 0x00..0x7F) |
| 40021..30 | 0x0014.. |
20.. | R/W | EEPROM | Button RGB Colors | Color codes | 0=OFF, 1=Red, 2=Green, 3=Blue, 4=Yellow, 5=Cyan, 6=Magenta. (Toilet uses 40029, Handset uses 40030) |
| 40050 | 0x0031 |
49 | R/W | RAM | Global Emergency Alarm | 0 |
1 = All-Red Strobe + Continuous Alarm, 0 = Normal |
| 40061..76 | 0x003C.. |
60.. | R/W | EEPROM | Priority Ranks | 1..16 / 0 | 1 = Highest (Code Blue), 16 = Lowest, 0 = Non-call |
| 40250 | 0x00F9 |
249 | READ | FLASH | Hardware Model Type | 104 |
104 = 4-Btn, 102 = 2-Btn, 101 = 1-Btn, 200 = Corridor Dome |
| 40251 | 0x00FA |
250 | READ | FLASH | Firmware Version | 0x0200 |
Embedded firmware version (v2.00) |
| 40252 | 0x00FB |
251 | READ | FLASH | Device Serial Number | 1001 |
Factory serial number / batch ID |
| 40253 | 0x00FC |
252 | R/W | RAM | Pairing Selection Flag | 0 |
1 = Physically Selected by technician (>3s hold) |
| 40254 | 0x00FD |
253 | R/W | EEPROM | Commissioning Status Flag | 0 |
0 = Factory New, 1 = Commissioned |
| 40255 | 0x00FE |
254 | R/W | RAM | Discovery Mode Command | 0 |
0x00AA (New), 0x00FF (All), 0 (Exit) |
| 40256 | 0x00FF |
255 | R/W | EEPROM | Modbus Slave ID | 255 |
Assigned Slave Address (1..247) |
40007, 40035, 40036 return 0x0000 on read; writes are silently ignored with NO exception. (On corridor dome units, register 40007 is assigned to Color Bed Slot 6.)
The C implementation below is complete, modular, and packed (53 Bytes EEPROM):
#ifndef SISCALL_MODBUS_FIRMWARE_H
#define SISCALL_MODBUS_FIRMWARE_H
#include <stdint.h>
#include <stdbool.h>
/* Action Code Bitmasks */
#define ACTION_DISABLED 0x0000
#define ACTION_CLEAR_ALL 0x0001
#define ACTION_CALL_SOLID 0x0002
#define ACTION_ACKNOWLEDGE 0x0004
#define ACTION_MUTE_BUZZER 0x0008
#define ACTION_STAFF_CHECKIN 0x0010
#define ACTION_CALL_BLINK 0x0020
#define ACTION_MULTI_CYCLE 0x0040
/* Nurse Station Remote & Diagnostic Command Bitmasks (Reg 40010 / Offset 9) */
#define CMD_REMOTE_CLEAR 0x0001 /* Bit 0: Remote Clear active calls */
#define CMD_REMOTE_ACK 0x0004 /* Bit 2: Remote Acknowledge */
#define CMD_REMOTE_MUTE 0x0008 /* Bit 3: Remote Mute / Snooze Buzzer */
#define CMD_DIAG_LED_TEST 0x0100 /* Bit 8: LED Self-Test (3s cycle) */
#define CMD_DIAG_BUZZER_TEST 0x0200 /* Bit 9: Buzzer Chirp Test (200ms) */
#define CMD_DIAG_EEPROM_CHECK 0x0400 /* Bit 10: EEPROM Checksum Verify */
/* Modbus Protocol Offsets (0-indexed) */
#define REG_LIVE_STATUS 0 /* 40001 (RO Latched) */
#define REG_SP_ENABLE_MASK 1 /* 40002 */
#define REG_LP_ENABLE_MASK 2 /* 40003 */
#define REG_LP_HOLD_TIME 3 /* 40004 */
#define REG_DEBOUNCE_TIME 4 /* 40005 */
#define REG_IDLE_GLOW_COLOR 5 /* 40006 */
#define REG_STATUS_CONTINUOUS 7 /* 40008 (RO Continuous) */
#define REG_CLEAR_MASK 8 /* 40009 (R/W Clear Mask) */
#define REG_REMOTE_CMD 9 /* 40010 (Downlink / Diagnostic) */
#define REG_GLOBAL_EMERGENCY 49 /* 40050 */
#define REG_DEVICE_MODEL 249 /* 40250 (RO Model Code) */
#define REG_FIRMWARE_VER 250 /* 40251 (RO Firmware Ver) */
#define REG_SERIAL_NO 251 /* 40252 (RO Serial No) */
#define REG_PAIRING_FLAG 252 /* 40253 (RAM Pairing Flag) */
#define REG_COMMISSIONED_FLAG 253 /* 40254 (EEPROM Flag) */
#define REG_DISCOVERY_CMD 254 /* 40255 (Discovery Command) */
#define REG_SLAVE_ID 255 /* 40256 (EEPROM Slave ID) */
/* Active Call Record with Snooze State */
typedef struct {
uint8_t btn;
uint8_t action_mask;
uint8_t color;
uint8_t priority;
bool muted; /* Snooze flag: true when silenced by MUTE_BUZZER (0x0008) */
} ActiveCall_t;
/* EEPROM Non-Volatile Memory Structure (53 Bytes Total, Packed) */
typedef struct __attribute__((packed)) {
uint8_t slave_id; /* 0x00: Reg 255 (Default 255) */
uint8_t is_commissioned; /* 0x01: Reg 253 (0=Factory, 1=Commissioned) */
uint16_t single_press_mask; /* 0x02..0x03: Reg 1 (Default 0xFFFF) */
uint16_t long_press_mask; /* 0x04..0x05: Reg 2 (Default 0xFFFF) */
uint16_t long_press_hold_ms; /* 0x06..0x07: Reg 3 (Default 2000ms) */
uint16_t debounce_ms; /* 0x08..0x09: Reg 4 (Default 50ms) */
uint8_t idle_glow_color; /* 0x0A: Reg 5 (Default 3=Blue) */
uint8_t btn_sp_action[4]; /* 0x0B..0x0E: Reg 10,12,14,16 (Default: 2, 32, 2, 4) */
uint8_t btn_lp_action[4]; /* 0x0F..0x12: Reg 11,13,15,17 (Default: 32, 32, 2, 1) */
uint8_t toilet_btn1_action[2]; /* 0x13..0x14: Reg 18,19 (Default: 32, 32) */
uint8_t toilet_btn2_action[2]; /* 0x15..0x16: Reg 36,37 (Default: 1, 1) */
uint8_t remote_action[4]; /* 0x17..0x1A: Reg 30..33 (Default: 2, 32, 4, 1) */
uint8_t btn_sp_color[4]; /* 0x1B..0x1E: Reg 20,22,24,26 (Default: 4, 1, 5, 2) */
uint8_t btn_lp_color[4]; /* 0x1F..0x22: Reg 21,23,25,27 (Default: 1, 1, 1, 0) */
uint8_t toilet_color; /* 0x23: Reg 28 (Default 4=Yellow) */
uint8_t handset_color; /* 0x24: Reg 29 (Default 4=Yellow) */
uint8_t btn_sp_priority[4]; /* 0x25..0x28: Reg 60,62,64,66 (Default: 3, 1, 4, 0) */
uint8_t btn_lp_priority[4]; /* 0x29..0x2C: Reg 61,63,65,67 (Default: 2, 1, 4, 0) */
uint8_t toilet_btn1_priority[2]; /* 0x2D..0x2E: Reg 68,69 (Default: 2, 1) */
uint8_t toilet_btn2_priority[2]; /* 0x2F..0x30: Reg 70,71 (Default: 0, 0) */
uint8_t remote_priority[4]; /* 0x31..0x34: Reg 72..75 (Default: 3, 2, 0, 0) */
} DeviceConfig_t;
extern DeviceConfig_t g_config;
extern uint16_t g_reg40001_live_status;
extern uint16_t g_reg40253_pairing_flag;
void EEPROM_LoadConfig(void);
void EEPROM_SaveConfig(void);
#endif /* SISCALL_MODBUS_FIRMWARE_H */
#include "siscall_modbus_firmware.h"
/* Single-Pass Button Event Pipeline with Multi-Action Bitmasks */
void handle_button_event(uint8_t btn_index, bool is_long_press) {
uint8_t action_mask = get_action_mask_from_eeprom(btn_index, is_long_press);
uint8_t led_color = get_color_code_from_eeprom(btn_index, is_long_press);
uint8_t prio_rank = get_priority_rank_from_eeprom(btn_index, is_long_press);
/* 0. Ignore if action code is 0 (0 = Disabled / No Action) */
if (action_mask == ACTION_DISABLED) {
return;
}
/* 1. CLEAR_ALL (0x0001): Immediate local 0ms call reset */
bool has_clear = (action_mask & ACTION_CLEAR_ALL) != 0;
if (has_clear) {
turn_off_all_button_leds();
set_hold_led(false);
stop_buzzer();
clear_all_active_calls(); /* Clears all call records and resets snooze states */
/* Do NOT return: allow non-call actions (e.g. Staff Check-in 0x0010) to execute */
}
/* 2. Call Actions (0x0002 Solid, 0x0020 Blink, 0x0040 Cycle) */
if (!has_clear && (action_mask & (ACTION_CALL_SOLID | ACTION_CALL_BLINK | ACTION_MULTI_CYCLE))) {
register_button_call(btn_index, action_mask, led_color, prio_rank); /* Initializes call->muted = false */
}
/* 3. Evaluate Winning Active Call & Render Visuals */
uint8_t win_btn = evaluate_highest_priority_active_call();
if (win_btn != NO_ACTIVE_CALL) {
uint8_t win_mask = get_action_mask(win_btn);
uint8_t win_color = get_color(win_btn);
if (win_mask & ACTION_CALL_BLINK) {
set_button_led_blink(win_btn, win_color);
} else if (win_mask & ACTION_CALL_SOLID) {
set_button_led_solid(win_btn, win_color);
}
if (win_mask & ACTION_MULTI_CYCLE) {
start_color_alternating_loop();
}
}
/* 4. ACKNOWLEDGE (0x0004): Acknowledges active winning call (only when pressed as non-call action) */
if ((action_mask & ACTION_ACKNOWLEDGE) && !(action_mask & (ACTION_CALL_SOLID | ACTION_CALL_BLINK | ACTION_MULTI_CYCLE))) {
if (win_btn != NO_ACTIVE_CALL) {
set_hold_led(true);
set_active_call_led_solid();
ActiveCall_t *call = get_active_call(win_btn);
if (call) call->muted = true; /* Acknowledge silences active alarm */
}
stop_buzzer();
}
/* 5. MUTE_BUZZER (0x0008) pressed alone as a snooze action (without call bits) */
if ((action_mask & ACTION_MUTE_BUZZER) && !(action_mask & (ACTION_CALL_SOLID | ACTION_CALL_BLINK | ACTION_MULTI_CYCLE))) {
if (win_btn != NO_ACTIVE_CALL) {
ActiveCall_t *call = get_active_call(win_btn);
if (call) call->muted = true; /* Snooze active winning call */
}
stop_buzzer();
}
/* 6. Dynamic Buzzer Evaluation from Winning Call */
if (win_btn != NO_ACTIVE_CALL) {
ActiveCall_t *win_call = get_active_call(win_btn);
/* Sound buzzer only if winning call is not silent (0x0008 in mask) and not snoozed */
if (win_call && !(win_call->action_mask & ACTION_MUTE_BUZZER) && !win_call->muted) {
start_buzzer_beep();
} else {
stop_buzzer();
}
} else {
stop_buzzer();
}
/* 7. STAFF_CHECKIN (0x0010): 3.0s confirmation LED + 100ms short chirp */
if (action_mask & ACTION_STAFF_CHECKIN) {
flash_confirmation_led(3000);
play_short_chirp();
}
/* 8. Latch Modbus Live Status Register 40001 once at the end */
latch_live_status_bit(btn_index, is_long_press);
}
/* Discovery Mode: Technician holds Button 1 for > 3s */
void on_button1_held_3_seconds_discovery(void) {
if (g_state.is_discovery_mode) {
g_reg40253_pairing_flag = 1; /* Dedicated pairing flag */
set_button_led(0, COLOR_YELLOW); /* Solid Yellow selection feedback */
}
}
#include "siscall_modbus_firmware.h"
#include <string.h>
uint16_t Modbus_CRC16(const uint8_t *buffer, uint16_t length) {
uint16_t crc = 0xFFFF;
for (uint16_t i = 0; i < length; i++) {
crc ^= buffer[i];
for (uint8_t j = 0; j < 8; j++) {
if (crc & 0x0001) crc = (crc >> 1) ^ 0xA001;
else crc >>= 1;
}
}
return crc;
}
void Modbus_ProcessPacket(const uint8_t *rx_buf, uint16_t rx_len, uint8_t *tx_buf, uint16_t *tx_len) {
if (rx_len < 4) return;
uint16_t received_crc = rx_buf[rx_len - 2] | (rx_buf[rx_len - 1] << 8);
if (Modbus_CRC16(rx_buf, rx_len - 2) != received_crc) return;
uint8_t slave_addr = rx_buf[0];
uint8_t function_code = rx_buf[1];
if (slave_addr != g_config.slave_id && slave_addr != 0x00 && slave_addr != 255) return;
uint16_t reg_addr = (rx_buf[2] << 8) | rx_buf[3];
/* FC 03: Read Holding Registers */
if (function_code == 0x03) {
uint16_t reg_count = (rx_buf[4] << 8) | rx_buf[5];
/* Address 255 check: only selected unit (Reg 40253 == 1) may respond */
if (slave_addr == 255 && g_reg40253_pairing_flag != 1) {
*tx_len = 0;
return;
}
tx_buf[0] = slave_addr;
tx_buf[1] = 0x03;
tx_buf[2] = reg_count * 2;
for (uint16_t i = 0; i < reg_count; i++) {
uint16_t curr_addr = reg_addr + i;
uint16_t value = 0;
if (curr_addr == REG_LIVE_STATUS) value = g_reg40001_live_status;
else if (curr_addr == REG_SP_ENABLE_MASK) value = g_config.single_press_mask;
else if (curr_addr == REG_LP_ENABLE_MASK) value = g_config.long_press_mask;
else if (curr_addr == REG_DEVICE_MODEL) value = 104; /* 4-Button Unit */
else if (curr_addr == REG_FIRMWARE_VER) value = 0x0200; /* v2.00 */
else if (curr_addr == REG_SERIAL_NO) value = 1001;
else if (curr_addr == REG_PAIRING_FLAG) value = g_reg40253_pairing_flag;
else if (curr_addr == REG_COMMISSIONED_FLAG) value = g_config.is_commissioned;
else if (curr_addr == REG_SLAVE_ID) value = g_config.slave_id;
tx_buf[3 + i * 2] = (value >> 8) & 0xFF;
tx_buf[4 + i * 2] = value & 0xFF;
}
uint16_t resp_len = 3 + reg_count * 2;
uint16_t crc = Modbus_CRC16(tx_buf, resp_len);
tx_buf[resp_len] = crc & 0xFF;
tx_buf[resp_len + 1] = (crc >> 8) & 0xFF;
*tx_len = resp_len + 2;
}
/* FC 06: Write Single Register */
else if (function_code == 0x06) {
uint16_t write_val = (rx_buf[4] << 8) | rx_buf[5];
/* Address 255 check: only selected unit may accept writes */
if (slave_addr == 255 && g_reg40253_pairing_flag != 1) {
*tx_len = 0;
return;
}
/* 1. Send Modbus echo reply FIRST before changing address or executing */
if (slave_addr != 0x00) {
memcpy(tx_buf, rx_buf, 8);
*tx_len = 8;
} else {
*tx_len = 0; /* Broadcast address 0 never replies */
}
/* 2. Process register writes */
if (reg_addr == REG_DISCOVERY_CMD) {
if (write_val == 0x00AA) {
if (g_config.is_commissioned == 0) g_state.is_discovery_mode = true;
} else if (write_val == 0x00FF) {
g_state.is_discovery_mode = true;
} else if (write_val == 0x0000) {
g_state.is_discovery_mode = false;
g_reg40253_pairing_flag = 0;
}
} else if (reg_addr == REG_SLAVE_ID && write_val >= 1 && write_val <= 247) {
g_config.slave_id = (uint8_t)write_val;
g_config.is_commissioned = 1;
g_reg40253_pairing_flag = 0;
EEPROM_SaveConfig(); /* Burn to EEPROM */
/* Solid Green 3s non-blocking feedback */
g_state.is_discovery_mode = false;
set_all_leds_green_3s();
} else if (reg_addr == REG_CLEAR_MASK) {
g_reg40001_live_status &= ~write_val; /* Atomically clear handled bits */
} else if (reg_addr == REG_REMOTE_CMD) {
if (write_val & CMD_REMOTE_CLEAR) handle_remote_clear_command();
if (write_val & CMD_REMOTE_ACK) handle_remote_ack_command();
if (write_val & CMD_REMOTE_MUTE) handle_remote_mute_command();
if (write_val & CMD_DIAG_LED_TEST) run_led_diagnostic_test();
if (write_val & CMD_DIAG_BUZZER_TEST) run_buzzer_diagnostic_test();
if (write_val & CMD_DIAG_EEPROM_CHECK) verify_eeprom_checksum();
} else if (reg_addr == REG_GLOBAL_EMERGENCY) {
if (write_val == 1) start_global_emergency_all_red_flash();
else stop_global_emergency();
}
}
}