RadioScheduler.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "RadioScheduler.h" | ||
| 2 | #include "esp_log.h" | ||
| 3 | |||
| 4 | // Logged as part of the radio code it came from. | ||
| 5 | static const char *TAG = "Somfy"; | ||
| 6 | |||
| 7 | #define TX_EVENT_QUEUE_SIZE (TX_JOB_QUEUE_SIZE * 3) | ||
| 8 | |||
| 9 | // Whether a job fits with spaces slots free. Normal jobs leave the reserved | ||
| 10 | // slots free for stops. | ||
| 11 | 277 | static bool admits(uint32_t spaces, bool priority) { | |
| 12 | 277 | return spaces > (priority ? 0u : (uint32_t)TX_RESERVED_SLOTS); | |
| 13 | } | ||
| 14 | 276 | uint32_t RadioScheduler::submit(somfy_tx_job_t &job, bool priority, uint16_t delayMs, uint32_t now) { | |
| 15 | 276 | job.ticket = 0; | |
| 16 | 276 | if(this->suspendMask) { | |
| 17 | ESP_LOGW(TAG, "Radio TX suspended, dropping transmission"); | ||
| 18 | 3 | return 0; | |
| 19 | } | ||
| 20 | 273 | if(!admits(TX_JOB_QUEUE_SIZE - this->count, priority)) { | |
| 21 | ESP_LOGE(TAG, "Radio TX queue full, dropping transmission"); | ||
| 22 | 3 | return 0; | |
| 23 | } | ||
| 24 | 270 | if(!this->outcomesFit()) { | |
| 25 | ESP_LOGE(TAG, "Radio TX outcomes not collected, dropping transmission"); | ||
| 26 | 2 | return 0; | |
| 27 | } | ||
| 28 | // 0 means no job so it is skipped on wrap. | ||
| 29 | 268 | if(++this->lastTicket == 0) ++this->lastTicket; | |
| 30 | 268 | job.ticket = this->lastTicket; | |
| 31 | 268 | job.priority = priority; | |
| 32 | 268 | job.front = priority && !job.repeater && !this->waitingFor(job.frame.remoteAddress); | |
| 33 | 268 | job.queuedAt = now; | |
| 34 | 268 | job.delayMs = delayMs; | |
| 35 | // Front jobs go out in the order they came, ahead of the back. | ||
| 36 | 268 | uint8_t at = this->count; | |
| 37 | 268 | if(job.front) { | |
| 38 | 13 | at = 0; | |
| 39 | 14 | while(at < this->count && this->jobs[at].front) at++; | |
| 40 | } | ||
| 41 | 333 | for(uint8_t i = this->count; i > at; i--) this->jobs[i] = this->jobs[i - 1]; | |
| 42 | 268 | this->jobs[at] = job; | |
| 43 | 268 | this->count++; | |
| 44 | 268 | return job.ticket; | |
| 45 | } | ||
| 46 | 5 | bool RadioScheduler::hasRoom() const { | |
| 47 | 5 | return !this->suspendMask && admits(TX_JOB_QUEUE_SIZE - this->count, false) && this->outcomesFit(); | |
| 48 | } | ||
| 49 | 273 | bool RadioScheduler::outcomesFit() const { | |
| 50 | // Each job not yet ended reports at most two outcomes: started, then how it | ||
| 51 | // ended. Counting repeater frames too keeps this simple and errs safe. | ||
| 52 | 273 | const uint32_t unended = this->count + (this->taken ? 1u : 0u) + 1u; | |
| 53 | 273 | return this->eventCount + 2u * unended <= TX_EVENT_QUEUE_SIZE; | |
| 54 | } | ||
| 55 | 46 | bool RadioScheduler::waitingFor(uint32_t remoteAddress) const { | |
| 56 | 112 | for(uint8_t i = 0; i < this->count; i++) { | |
| 57 | 99 | if(!this->jobs[i].repeater && this->jobs[i].frame.remoteAddress == remoteAddress) return true; | |
| 58 | } | ||
| 59 | 13 | return false; | |
| 60 | } | ||
| 61 | 112 | void RadioScheduler::remove(uint8_t i) { | |
| 62 | 167 | for(; i + 1 < this->count; i++) this->jobs[i] = this->jobs[i + 1]; | |
| 63 | 112 | this->count--; | |
| 64 | 112 | } | |
| 65 | 126 | bool RadioScheduler::next(uint32_t now, somfy_tx_job_t &job, uint32_t &waitMs) { | |
| 66 | 126 | waitMs = RADIO_WAIT_FOREVER; | |
| 67 | 126 | if(this->count == 0) return false; | |
| 68 | // Jobs go out in order, so a delayed job holds up the ones behind it. A | ||
| 69 | // front job queued meanwhile is ahead of it. | ||
| 70 | 122 | const somfy_tx_job_t &head = this->jobs[0]; | |
| 71 | 122 | if(head.delayMs != 0) { | |
| 72 | // The delay lets the motor settle, so it counts from the job ahead of it | ||
| 73 | // going off air when that came after the job was queued. Ages are unsigned | ||
| 74 | // so this works across a millis() wrap. | ||
| 75 | 18 | uint32_t waited = now - head.queuedAt; | |
| 76 | 18 | if(this->endedOnce && now - this->lastEnded < waited) waited = now - this->lastEnded; | |
| 77 | 18 | if(waited < head.delayMs) { | |
| 78 | 10 | waitMs = head.delayMs - waited; | |
| 79 | 10 | return false; | |
| 80 | } | ||
| 81 | } | ||
| 82 | 112 | job = head; | |
| 83 | 112 | this->remove(0); | |
| 84 | 112 | this->taken = job.ticket; | |
| 85 | 112 | this->takenFlushed = false; | |
| 86 | 112 | this->takenJob = job; | |
| 87 | 112 | return true; | |
| 88 | } | ||
| 89 | 108 | bool RadioScheduler::start(uint32_t ticket, uint32_t now) { | |
| 90 | 108 | if(ticket == 0 || ticket != this->taken) return false; | |
| 91 | 107 | if(this->takenFlushed) { | |
| 92 | // Flushed after the radio task took it. | ||
| 93 | 1 | this->post(tx_event_t::dropped, this->takenJob, now); | |
| 94 | 1 | this->taken = 0; | |
| 95 | 1 | return false; | |
| 96 | } | ||
| 97 | 106 | this->post(tx_event_t::started, this->takenJob, now); | |
| 98 | 106 | return true; | |
| 99 | } | ||
| 100 | 5 | bool RadioScheduler::keepSending(uint32_t ticket) const { | |
| 101 | 5 | return ticket != 0 && ticket == this->taken && !this->takenFlushed; | |
| 102 | } | ||
| 103 | 107 | void RadioScheduler::ended(uint32_t ticket, bool complete, uint32_t now) { | |
| 104 | 107 | if(ticket == 0 || ticket != this->taken) return; | |
| 105 | 106 | this->post(complete ? tx_event_t::finished : tx_event_t::aborted, this->takenJob, now); | |
| 106 | 106 | this->taken = 0; | |
| 107 | 106 | this->lastEnded = now; | |
| 108 | 106 | this->endedOnce = true; | |
| 109 | } | ||
| 110 | 9 | void RadioScheduler::flush(uint32_t now) { | |
| 111 | // Every discarded job is reported as dropped. | ||
| 112 | 45 | for(uint8_t i = 0; i < this->count; i++) this->post(tx_event_t::dropped, this->jobs[i], now); | |
| 113 | 9 | this->count = 0; | |
| 114 | 9 | if(this->taken) this->takenFlushed = true; | |
| 115 | 9 | } | |
| 116 | 3 | void RadioScheduler::suspend(uint8_t reason, uint32_t now) { | |
| 117 | 3 | this->suspendMask |= reason; | |
| 118 | 3 | this->flush(now); | |
| 119 | 3 | } | |
| 120 | 2 | void RadioScheduler::resume(uint8_t reason) { this->suspendMask &= ~reason; } | |
| 121 | 249 | void RadioScheduler::post(tx_event_t type, const somfy_tx_job_t &job, uint32_t at) { | |
| 122 | // Repeater frames don't belong to any of our remotes. | ||
| 123 | 249 | if(job.repeater) return; | |
| 124 | 244 | if(this->eventCount >= TX_EVENT_QUEUE_SIZE) { | |
| 125 | // submit() leaves room for every outcome so this can't happen short of a bug. | ||
| 126 | ESP_LOGE(TAG, "Radio TX event queue full, outcome lost"); | ||
| 127 | ✗ | return; | |
| 128 | } | ||
| 129 | 244 | somfy_tx_event_t &evt = this->events[(this->eventHead + this->eventCount) % TX_EVENT_QUEUE_SIZE]; | |
| 130 | 244 | evt.type = type; | |
| 131 | 244 | evt.cmd = job.frame.cmd; | |
| 132 | 244 | evt.remoteAddress = job.frame.remoteAddress; | |
| 133 | 244 | evt.ticket = job.ticket; | |
| 134 | 244 | evt.at = at; | |
| 135 | 244 | this->eventCount++; | |
| 136 | } | ||
| 137 | 200 | bool RadioScheduler::popEvent(somfy_tx_event_t &evt) { | |
| 138 | 200 | if(this->eventCount == 0) return false; | |
| 139 | 188 | evt = this->events[this->eventHead]; | |
| 140 | 188 | this->eventHead = (uint8_t)((this->eventHead + 1) % TX_EVENT_QUEUE_SIZE); | |
| 141 | 188 | this->eventCount--; | |
| 142 | 188 | return true; | |
| 143 | } | ||
| 144 |