#include "CompanionNotificationPolicy.h" namespace mesh { namespace notify { namespace { void valueText(const Rule& r,uint16_t f,char* out,size_t size) { if(!(r.fields&f)) { snprintf(out,size,"inherit");return; } if(f==FVibe)formatPulse(r.vibration,out,size); else if(f==FLed)formatPulse(r.led,out,size); else if(f==FSound)snprintf(out,size,"%s",r.sound[0]?r.sound:"off"); else if(f==FScreen)snprintf(out,size,"%s",r.screen?"on":"off"); else if(f==FGpio) { if(r.pin<0)snprintf(out,size,"off"); else { size_t n=snprintf(out,size,"%d:",int(r.pin));if(n1||(s.outputs&~31))return false; for(unsigned i=0;i1)return false;if(!r.used)continue; if(r.selector.kind>Channel||r.selector.when>Offline||r.fields>511||r.stop>Never||r.remote>1 ||!r.gap||r.gap>60000||r.screen<-1||r.screen>1||r.pin<-1||r.pin>63)return false; if((r.fields&FRemote)&&(r.selector.kind!=Contact||r.selector.when!=Any))return false; const Pulse* patterns[]={&r.vibration,&r.led,&r.gpio}; for(const Pulse* p:patterns) { if(p->count>MAX_PULSES)return false; for(unsigned j=0;jcount;++j)if(!p->ms[j]||p->ms[j]>60000)return false; } uint32_t total; if(!memchr(r.sound,0,sizeof(r.sound))||!soundDuration(r.sound,total))return false; // A pin valid on an older image may now belong to GPS or storage. if(r.pin>=0&&!sink.gpioAvailable(r.pin))return false; for(unsigned j=0;j(memchr(p,'=',end-p)); if(!equal||equal-p>=16||end-equal-1>=96||equal==p)return false; char key[16]={},value[96]={};memcpy(key,p,equal-p);memcpy(value,equal+1,end-equal-1); uint16_t f=field(key); // No pin selection, permission/stop changes, or persistent writes over DM. // Unknown or duplicated fields reject the whole notification string. if(!f||f==FGpio||f==FRemote||f==FStop||(next.fields&f)||!strcmp(value,"inherit")||!setField(next,f,value))return false; p=*end?end+1:end; if(*end&&!*p)return false; } if(!(next.fields&(FVibe|FSound|FLed|FScreen)))return false; if(timestamp) { SeenRemote& seen=remote_seen[remote_seen_next++%8]; memcpy(seen.key,contact,32);seen.timestamp=timestamp;seen.hash=hash; } stop();playing=next;started=now;cycles=0;running=true;remote_playing=true;remote_started=now; if(playing.fields&FLed)sink.pulse(Led,false,-1); if(playing.fields&FVibe)sink.pulse(Vibration,false,-1); startCycle();return true; } bool Controller::handle(const char* command,char* reply,size_t size,uint32_t now,bool connected) { bool set=!strncmp(command,"set notify.",11),get=!strncmp(command,"get notify.",11); bool test=!strncmp(command,"notify.test ",12),del=!strncmp(command,"notify.delete ",14); if(!strcmp(command,"notify.stop")) { stop();snprintf(reply,size,"OK - alerts stopped");return true; } if(!strcmp(command,"get notify")||!strcmp(command,"get notify.status")) { snprintf(reply,size,"> enabled=%s outputs=%u supported=%u active=%s rules=%u", prefs.enabled?"on":"off",prefs.outputs,sink.capabilities(),running?"on":"off",MAX_RULES);return true; } if(!strcmp(command,"get notify.gpio.pins")) { size_t n=snprintf(reply,size,"> GPIOs:"); for(unsigned pin=0;pin<64&&n slots:"); for(unsigned i=0;i=sizeof(key)) { snprintf(reply,size,"Error: unknown notification setting");return true; } memcpy(key,p,len);p+=len; if(*p==' ')++p; if(!strcmp(key,"enabled")||(!*p&&get)||(set&&(!strcmp(p,"on")||!strcmp(p,"off")))) { uint16_t f=field(key); if(strcmp(key,"enabled") && (f==0||f>FGpio)) { snprintf(reply,size,"Error: unknown notification setting");return true; } if(get) { snprintf(reply,size,"> %s",(!strcmp(key,"enabled")?prefs.enabled:bool(prefs.outputs&f))?"on":"off");return true; } if(strcmp(p,"on")&&strcmp(p,"off")) { snprintf(reply,size,"Error: use on|off");return true; } bool on=!strcmp(p,"on"); if(on&&f&&!(sink.capabilities()&f)) { snprintf(reply,size,"Error: output unsupported on this build");return true; } uint8_t enabled=prefs.enabled,outputs=prefs.outputs; if(!strcmp(key,"enabled"))prefs.enabled=on; else { prefs.enabled=1;if(on)prefs.outputs|=f;else prefs.outputs&=~f; } if(!store.save(prefs)) { prefs.enabled=enabled;prefs.outputs=outputs;snprintf(reply,size,"Error: save failed"); } else { stop();snprintf(reply,size,"OK"); } return true; } } size_t len=strcspn(p," "); if(!len||len>=sizeof(target)) { snprintf(reply,size,"Error: invalid target");return true; } memcpy(target,p,len);p+=len; if(*p==' ')value=p+1; Selector selector; if(!parseSelector(target,selector)) { snprintf(reply,size,"Error: use all|contact:64hex|room:64hex|channel:32hex, optional @connected|@disconnected");return true; } if(test) { if(value) { snprintf(reply,size,"Error: unexpected argument");return true; } bool state=selector.when==Any?connected:selector.when==Online; bool matched=message(selector.kind==Contact?selector.key:nullptr,selector.kind==Channel?selector.key:nullptr,state,now); snprintf(reply,size,"%s",matched?"OK - alert test started":"Error: no enabled matching rule");return true; } int slot=-1,empty=-1; for(unsigned i=0;i=0&&!sink.gpioAvailable(candidate.pin)) { snprintf(reply,size,"Error: GPIO reserved or unavailable; use get notify.gpio.pins");return true; } uint8_t enabled=prefs.enabled; prefs.rules[slot]=candidate;prefs.enabled=1; if(!store.save(prefs)) { prefs.rules[slot]=previous;prefs.enabled=enabled;snprintf(reply,size,"Error: save failed"); } else { stop();snprintf(reply,size,"OK"); } return true; } } }