3 #if defined(PLATFORM_ESP8266) || defined(PLATFORM_ESP32)
5 #if defined(PLATFORM_ESP32)
10 #include <ESP8266WiFi.h>
11 #include <ESP8266mDNS.h>
12 #define wifi_mode_t WiFiMode_t
14 #include <DNSServer.h>
17 #include <StreamString.h>
19 #include <ESPAsyncWebServer.h>
22 #include "POWERMGNT.h"
28 #include "WebContent.h"
30 #if defined(Regulatory_Domain_AU_915) || defined(Regulatory_Domain_EU_868) || defined(Regulatory_Domain_IN_866) || defined(Regulatory_Domain_FCC_915) || defined(Regulatory_Domain_AU_433) || defined(Regulatory_Domain_EU_433)
31 #include "SX127xDriver.h"
32 extern SX127xDriver Radio
;
35 #if defined(Regulatory_Domain_ISM_2400)
36 #include "SX1280Driver.h"
37 extern SX1280Driver Radio
;
41 #if defined(TARGET_TX)
42 extern TxConfig config
;
44 extern RxConfig config
;
46 extern unsigned long rebootTime
;
48 static bool wifiStarted
= false;
49 bool webserverPreventAutoStart
= false;
50 extern bool InBindingMode
;
52 static wl_status_t laststatus
= WL_IDLE_STATUS
;
53 static volatile WiFiMode_t wifiMode
= WIFI_OFF
;
54 static volatile WiFiMode_t changeMode
= WIFI_OFF
;
55 static volatile unsigned long changeTime
= 0;
57 static const byte DNS_PORT
= 53;
58 static IPAddress
netMsk(255, 255, 255, 0);
59 static DNSServer dnsServer
;
60 static IPAddress ipAddress
;
62 static AsyncWebServer
server(80);
63 static bool servicesStarted
= false;
65 static bool target_seen
= false;
66 static uint8_t target_pos
= 0;
67 static String target_found
;
68 static bool target_complete
= false;
69 static bool force_update
= false;
70 static uint32_t totalSize
;
73 static boolean
isIp(String str
)
75 for (size_t i
= 0; i
< str
.length(); i
++)
77 int c
= str
.charAt(i
);
78 if (c
!= '.' && (c
< '0' || c
> '9'))
87 static String
toStringIp(IPAddress ip
)
90 for (int i
= 0; i
< 3; i
++)
92 res
+= String((ip
>> (8 * i
)) & 0xFF) + ".";
94 res
+= String(((ip
>> 8 * 3)) & 0xFF);
98 static bool captivePortal(AsyncWebServerRequest
*request
)
100 extern const char *wifi_hostname
;
102 if (!isIp(request
->host()) && request
->host() != (String(wifi_hostname
) + ".local"))
104 DBGLN("Request redirected to captive portal");
105 request
->redirect(String("http://") + toStringIp(request
->client()->localIP()));
111 static void WebUpdateSendCSS(AsyncWebServerRequest
*request
)
113 AsyncWebServerResponse
*response
= request
->beginResponse_P(200, "text/css", (uint8_t*)CSS
, sizeof(CSS
));
114 response
->addHeader("Content-Encoding", "gzip");
115 request
->send(response
);
118 static void WebUpdateSendJS(AsyncWebServerRequest
*request
)
120 AsyncWebServerResponse
*response
= request
->beginResponse_P(200, "text/javascript", (uint8_t*)SCAN_JS
, sizeof(SCAN_JS
));
121 response
->addHeader("Content-Encoding", "gzip");
122 request
->send(response
);
125 static void WebUpdateSendFlag(AsyncWebServerRequest
*request
)
127 AsyncWebServerResponse
*response
= request
->beginResponse_P(200, "image/svg+xml", (uint8_t*)FLAG
, sizeof(FLAG
));
128 response
->addHeader("Content-Encoding", "gzip");
129 request
->send(response
);
132 static void WebUpdateHandleRoot(AsyncWebServerRequest
*request
)
134 if (captivePortal(request
))
135 { // If captive portal redirect instead of displaying the page.
138 force_update
= request
->hasArg("force");
139 AsyncWebServerResponse
*response
= request
->beginResponse_P(200, "text/html", (uint8_t*)INDEX_HTML
, sizeof(INDEX_HTML
));
140 response
->addHeader("Cache-Control", "no-cache, no-store, must-revalidate");
141 response
->addHeader("Pragma", "no-cache");
142 response
->addHeader("Expires", "-1");
143 response
->addHeader("Content-Encoding", "gzip");
144 request
->send(response
);
147 #if defined(GPIO_PIN_PWM_OUTPUTS)
148 constexpr uint8_t SERVO_PINS
[] = GPIO_PIN_PWM_OUTPUTS
;
149 constexpr uint8_t SERVO_COUNT
= ARRAY_SIZE(SERVO_PINS
);
151 static String
WebGetPwmStr()
153 // Output is raw integers, the Javascript side needs to parse it
154 // ,"pwm":[49664,50688,51200] = 3 channels, 0=512, 1=512, 2=0
155 String
pwmStr(",\"pwm\":[");
156 for (uint8_t ch
=0; ch
<SERVO_COUNT
; ++ch
)
160 pwmStr
.concat(config
.GetPwmChannel(ch
)->raw
);
167 static void WebUpdatePwm(AsyncWebServerRequest
*request
)
169 String pwmStr
= request
->arg("pwm");
170 if (pwmStr
.isEmpty())
172 request
->send(400, "text/plain", "Empty pwm parameter");
176 // parse out the integers representing the PWM values
177 // strtok will modify the string as it parses
178 char *token
= strtok((char *)pwmStr
.c_str(), ",");
180 while (token
!= nullptr && channel
< SERVO_COUNT
)
182 uint16_t val
= atoi(token
);
183 DBGLN("PWMch(%u)=%u", channel
, val
);
184 config
.SetPwmChannelRaw(channel
, val
);
186 token
= strtok(nullptr, ",");
189 request
->send(200, "text/plain", "PWM outputs updated");
193 static void WebUpdateSendMode(AsyncWebServerRequest
*request
)
195 String s
= String("{\"ssid\":\"") + config
.GetSSID() + "\",\"mode\":\"";
196 if (wifiMode
== WIFI_STA
) {
201 #if defined(TARGET_RX)
202 s
+= ",\"modelid\":" + String(config
.GetModelId());
204 #if defined(GPIO_PIN_PWM_OUTPUTS)
208 request
->send(200, "application/json", s
);
211 static void WebUpdateGetTarget(AsyncWebServerRequest
*request
)
213 String s
= String("{\"target\":\"") + (const char *)&target_name
[4] + "\",\"version\": \"" + VERSION
+ "\"}";
214 request
->send(200, "application/json", s
);
217 static void WebUpdateSendNetworks(AsyncWebServerRequest
*request
)
219 int numNetworks
= WiFi
.scanComplete();
220 if (numNetworks
>= 0) {
221 DBGLN("Found %d networks", numNetworks
);
224 for(int i
=0 ; i
<numNetworks
; i
++) {
225 String w
= WiFi
.SSID(i
);
226 DBGLN("found %s", w
.c_str());
227 if (vs
.find(w
)==vs
.end() && w
.length()>0) {
228 if (!vs
.empty()) s
+= ",";
229 s
+= "\"" + w
+ "\"";
234 request
->send(200, "application/json", s
);
236 request
->send(204, "application/json", "[]");
240 static void sendResponse(AsyncWebServerRequest
*request
, const String
&msg
, WiFiMode_t mode
) {
241 AsyncWebServerResponse
*response
= request
->beginResponse(200, "text/plain", msg
);
242 response
->addHeader("Connection", "close");
243 request
->send(response
);
244 request
->client()->close();
245 changeTime
= millis();
249 static void WebUpdateAccessPoint(AsyncWebServerRequest
*request
)
251 DBGLN("Starting Access Point");
252 String msg
= String("Access Point starting, please connect to access point '") + wifi_ap_ssid
+ "' with password '" + wifi_ap_password
+ "'";
253 sendResponse(request
, msg
, WIFI_AP
);
256 static void WebUpdateConnect(AsyncWebServerRequest
*request
)
258 DBGLN("Connecting to home network");
259 String msg
= String("Connecting to network '") + config
.GetSSID() + "', connect to http://" +
260 wifi_hostname
+ ".local from a browser on that network";
261 sendResponse(request
, msg
, WIFI_STA
);
264 static void WebUpdateSetHome(AsyncWebServerRequest
*request
)
266 String ssid
= request
->arg("network");
267 String password
= request
->arg("password");
269 DBGLN("Setting home network %s", ssid
.c_str());
270 config
.SetSSID(ssid
.c_str());
271 config
.SetPassword(password
.c_str());
273 WebUpdateConnect(request
);
276 static void WebUpdateForget(AsyncWebServerRequest
*request
)
278 DBGLN("Forget home network");
280 config
.SetPassword("");
282 String msg
= String("Home network forgotten, please connect to access point '") + wifi_ap_ssid
+ "' with password '" + wifi_ap_password
+ "'";
283 sendResponse(request
, msg
, WIFI_AP
);
286 #if defined(TARGET_RX)
287 static void WebUpdateModelId(AsyncWebServerRequest
*request
)
289 long modelid
= request
->arg("modelid").toInt();
290 if (modelid
< 0 || modelid
> 63) modelid
= 255;
291 DBGLN("Setting model match id %u", (uint8_t)modelid
);
292 config
.SetModelId((uint8_t)modelid
);
295 AsyncWebServerResponse
*response
= request
->beginResponse(200, "text/plain", "Model Match updated, rebooting receiver");
296 response
->addHeader("Connection", "close");
297 request
->send(response
);
298 request
->client()->close();
299 rebootTime
= millis() + 100;
303 static void WebUpdateHandleNotFound(AsyncWebServerRequest
*request
)
305 if (captivePortal(request
))
306 { // If captive portal redirect instead of displaying the error page.
309 String message
= F("File Not Found\n\n");
310 message
+= F("URI: ");
311 message
+= request
->url();
312 message
+= F("\nMethod: ");
313 message
+= (request
->method() == HTTP_GET
) ? "GET" : "POST";
314 message
+= F("\nArguments: ");
315 message
+= request
->args();
318 for (uint8_t i
= 0; i
< request
->args(); i
++)
320 message
+= String(F(" ")) + request
->argName(i
) + F(": ") + request
->arg(i
) + F("\n");
322 AsyncWebServerResponse
*response
= request
->beginResponse(404, "text/plain", message
);
323 response
->addHeader("Cache-Control", "no-cache, no-store, must-revalidate");
324 response
->addHeader("Pragma", "no-cache");
325 response
->addHeader("Expires", "-1");
326 request
->send(response
);
329 static void WebUploadResponseHandler(AsyncWebServerRequest
*request
) {
330 if (Update
.hasError()) {
331 StreamString p
= StreamString();
332 Update
.printError(p
);
334 DBGLN("Failed to upload firmware: %s", p
.c_str());
335 AsyncWebServerResponse
*response
= request
->beginResponse(200, "application/json", String("{\"status\": \"error\", \"msg\": \"") + p
+ "\"}");
336 response
->addHeader("Connection", "close");
337 request
->send(response
);
338 request
->client()->close();
341 DBGLN("Update complete, rebooting");
342 AsyncWebServerResponse
*response
= request
->beginResponse(200, "application/json", "{\"status\": \"ok\", \"msg\": \"Update complete. Please wait for LED to resume blinking before disconnecting power.\"}");
343 response
->addHeader("Connection", "close");
344 request
->send(response
);
345 request
->client()->close();
346 rebootTime
= millis() + 200;
348 String message
= String("{\"status\": \"mismatch\", \"msg\": \"<b>Current target:</b> ") + (const char *)&target_name
[4] + ".<br>";
349 if (target_found
.length() != 0) {
350 message
+= "<b>Uploaded image:</b> " + target_found
+ ".<br/>";
352 message
+= "<br/>Flashing the wrong firmware may lock or damage your device.\"}";
353 request
->send(200, "application/json", message
);
358 static void WebUploadDataHandler(AsyncWebServerRequest
*request
, const String
& filename
, size_t index
, uint8_t *data
, size_t len
, bool final
) {
360 DBGLN("Update: %s", filename
.c_str());
361 #if defined(PLATFORM_ESP8266)
362 Update
.runAsync(true);
363 uint32_t maxSketchSpace
= (ESP
.getFreeSketchSpace() - 0x1000) & 0xFFFFF000;
364 DBGLN("Free space = %u", maxSketchSpace
);
365 if (!Update
.begin(maxSketchSpace
, U_FLASH
)){//start with max available size
367 if (!Update
.begin()) { //start with max available size
369 Update
.printError(Serial
);
372 target_found
.clear();
373 target_complete
= false;
378 DBGVLN("writing %d", len
);
379 if (Update
.write(data
, len
) == len
) {
380 if (force_update
|| (totalSize
== 0 && *data
== 0x1F))
383 for (size_t i
=0 ; i
<len
;i
++) {
384 if (!target_complete
&& (target_pos
>= 4 || target_found
.length() > 0)) {
385 if (target_pos
== 4) {
386 target_found
.clear();
388 if (data
[i
] == 0 || target_found
.length() > 50) {
389 target_complete
= true;
392 target_found
+= (char)data
[i
];
395 if (data
[i
] == target_name
[target_pos
]) {
397 if (target_pos
>= target_name_size
) {
402 target_pos
= 0; // Startover
409 if (final
&& !Update
.getError()) {
412 if (Update
.end(true)) { //true to set the size to the current progress
413 DBGLN("Upload Success: %ubytes\nPlease wait for LED to resume blinking before disconnecting power", totalSize
);
415 Update
.printError(Serial
);
421 static void WebUploadForceUpdateHandler(AsyncWebServerRequest
*request
) {
423 if (request
->arg("action").equals("confirm")) {
424 if (Update
.end(true)) { //true to set the size to the current progress
425 DBGLN("Upload Success: %ubytes\nPlease wait for LED to turn resume blinking before disconnecting power", totalSize
);
427 Update
.printError(Serial
);
429 WebUploadResponseHandler(request
);
431 #if defined(PLATFORM_ESP32)
434 request
->send(200, "application/json", "{\"status\": \"ok\", \"msg\": \"Update cancelled\"}");
438 static size_t getFirmwareChunk(uint8_t *data
, size_t len
, size_t pos
)
441 uint8_t alignedBuffer
[7];
442 if ((uintptr_t)data
% 4 != 0)
444 // If data is not aligned, read aligned byes using the local buffer and hope the next call will be aligned
445 dst
= (uint8_t *)((uint32_t)alignedBuffer
/ 4 * 4);
450 // Otherwise just make sure len is a multiple of 4 and smaller than a sector
452 len
= constrain((len
/ 4) * 4, 4, SPI_FLASH_SEC_SIZE
);
455 ESP
.flashRead(pos
, (uint32_t *)dst
, len
);
457 // If using local stack buffer, move the 4 bytes into the passed buffer
458 // data is known to not be aligned so it is moved byte-by-byte instead of as uint32_t*
459 if ((void *)dst
!= (void *)data
)
461 for (unsigned b
=len
; b
>0; --b
)
467 static void WebUpdateGetFirmware(AsyncWebServerRequest
*request
) {
468 AsyncWebServerResponse
*response
= request
->beginResponse("application/octet-stream", (size_t)ESP
.getSketchSize(), &getFirmwareChunk
);
469 String filename
= String("attachment; filename=\"") + (const char *)&target_name
[4] + "_" + VERSION
+ ".bin\"";
470 response
->addHeader("Content-Disposition", filename
);
471 request
->send(response
);
474 static void wifiOff()
477 WiFi
.disconnect(true);
479 #if defined(PLATFORM_ESP8266)
480 WiFi
.forceSleepBegin();
484 static void startWiFi(unsigned long now
)
490 // Set transmit power to minimum
491 POWERMGNT::setPower(MinPower
);
492 if (connectionState
< FAILURE_STATES
) {
493 connectionState
= wifiUpdate
;
496 DBGLN("Stopping Radio");
499 DBGLN("Begin Webupdater");
501 WiFi
.persistent(false);
504 #if defined(PLATFORM_ESP8266)
505 WiFi
.setOutputPower(13);
506 WiFi
.setPhyMode(WIFI_PHY_MODE_11N
);
507 #elif defined(PLATFORM_ESP32)
508 WiFi
.setTxPower(WIFI_POWER_13dBm
);
510 if (config
.GetSSID()[0]==0 && home_wifi_ssid
[0]!=0) {
511 config
.SetSSID(home_wifi_ssid
);
512 config
.SetPassword(home_wifi_password
);
515 if (config
.GetSSID()[0]==0) {
517 changeMode
= WIFI_AP
;
520 changeMode
= WIFI_STA
;
522 laststatus
= WL_DISCONNECTED
;
526 static void startMDNS()
528 if (!MDNS
.begin(wifi_hostname
))
530 DBGLN("Error starting mDNS");
534 String instance
= String(wifi_hostname
) + "_" + WiFi
.macAddress();
535 instance
.replace(":", "");
536 #ifdef PLATFORM_ESP8266
537 // We have to do it differently on ESP8266 as setInstanceName has the side-effect of chainging the hostname!
538 MDNS
.setInstanceName(wifi_hostname
);
539 MDNSResponder::hMDNSService service
= MDNS
.addService(instance
.c_str(), "http", "tcp", 80);
540 MDNS
.addServiceTxt(service
, "vendor", "elrs");
541 MDNS
.addServiceTxt(service
, "target", (const char *)&target_name
[4]);
542 MDNS
.addServiceTxt(service
, "version", VERSION
);
543 MDNS
.addServiceTxt(service
, "options", String(FPSTR(compile_options
)).c_str());
544 MDNS
.addServiceTxt(service
, "type", "rx");
545 // If the probe result fails because there is another device on the network with the same name
546 // use our unique instance name as the hostname. A better way to do this would be to use
547 // MDNSResponder::indexDomain and change wifi_hostname as well.
548 MDNS
.setHostProbeResultCallback([instance
](const char* p_pcDomainName
, bool p_bProbeResult
) {
549 if (!p_bProbeResult
) {
550 WiFi
.hostname(instance
);
551 MDNS
.setInstanceName(instance
);
555 MDNS
.setInstanceName(instance
);
556 MDNS
.addService("http", "tcp", 80);
557 MDNS
.addServiceTxt("http", "tcp", "vendor", "elrs");
558 MDNS
.addServiceTxt("http", "tcp", "target", (const char *)&target_name
[4]);
559 MDNS
.addServiceTxt("http", "tcp", "device", device_name
);
560 MDNS
.addServiceTxt("http", "tcp", "version", VERSION
);
561 MDNS
.addServiceTxt("http", "tcp", "options", String(FPSTR(compile_options
)).c_str());
562 MDNS
.addServiceTxt("http", "tcp", "type", "tx");
566 static void startServices()
568 if (servicesStarted
) {
569 #if defined(PLATFORM_ESP32)
576 server
.on("/", WebUpdateHandleRoot
);
577 server
.on("/main.css", WebUpdateSendCSS
);
578 server
.on("/scan.js", WebUpdateSendJS
);
579 server
.on("/logo.svg", WebUpdateSendFlag
);
580 server
.on("/mode.json", WebUpdateSendMode
);
581 server
.on("/networks.json", WebUpdateSendNetworks
);
582 server
.on("/sethome", WebUpdateSetHome
);
583 server
.on("/forget", WebUpdateForget
);
584 server
.on("/connect", WebUpdateConnect
);
585 server
.on("/access", WebUpdateAccessPoint
);
586 server
.on("/target", WebUpdateGetTarget
);
587 server
.on("/firmware.bin", WebUpdateGetFirmware
);
589 server
.on("/generate_204", WebUpdateHandleRoot
); // handle Andriod phones doing shit to detect if there is 'real' internet and possibly dropping conn.
590 server
.on("/gen_204", WebUpdateHandleRoot
);
591 server
.on("/library/test/success.html", WebUpdateHandleRoot
);
592 server
.on("/hotspot-detect.html", WebUpdateHandleRoot
);
593 server
.on("/connectivity-check.html", WebUpdateHandleRoot
);
594 server
.on("/check_network_status.txt", WebUpdateHandleRoot
);
595 server
.on("/ncsi.txt", WebUpdateHandleRoot
);
596 server
.on("/fwlink", WebUpdateHandleRoot
);
598 server
.on("/update", HTTP_POST
, WebUploadResponseHandler
, WebUploadDataHandler
);
599 server
.on("/forceupdate", WebUploadForceUpdateHandler
);
601 #if defined(TARGET_RX)
602 server
.on("/model", WebUpdateModelId
);
604 #if defined(GPIO_PIN_PWM_OUTPUTS)
605 server
.on("/pwm", WebUpdatePwm
);
608 server
.onNotFound(WebUpdateHandleNotFound
);
612 dnsServer
.start(DNS_PORT
, "*", ipAddress
);
613 dnsServer
.setErrorReplyCode(DNSReplyCode::NoError
);
617 servicesStarted
= true;
618 DBGLN("HTTPUpdateServer ready! Open http://%s.local in your browser", wifi_hostname
);
621 static void HandleWebUpdate()
623 unsigned long now
= millis();
624 wl_status_t status
= WiFi
.status();
625 if (status
!= laststatus
&& wifiMode
== WIFI_STA
) {
626 DBGLN("WiFi status %d", status
);
628 case WL_NO_SSID_AVAIL
:
629 case WL_CONNECT_FAILED
:
630 case WL_CONNECTION_LOST
:
632 changeMode
= WIFI_AP
;
634 case WL_DISCONNECTED
: // try reconnection
642 if (status
!= WL_CONNECTED
&& wifiMode
== WIFI_STA
&& (now
- changeTime
) > 30000) {
644 changeMode
= WIFI_AP
;
645 DBGLN("Connection failed %d", status
);
647 if (changeMode
!= wifiMode
&& changeMode
!= WIFI_OFF
&& (now
- changeTime
) > 500) {
650 DBGLN("Changing to AP mode");
653 #if defined(PLATFORM_ESP8266)
654 WiFi
.mode(WIFI_AP_STA
);
659 WiFi
.softAPConfig(ipAddress
, ipAddress
, netMsk
);
660 WiFi
.softAP(wifi_ap_ssid
, wifi_ap_password
);
661 WiFi
.scanNetworks(true);
665 DBGLN("Connecting to home network '%s'", config
.GetSSID());
668 WiFi
.setHostname(wifi_hostname
); // hostname must be set after the mode is set to STA
670 WiFi
.begin(config
.GetSSID(), config
.GetPassword());
675 #if defined(PLATFORM_ESP8266)
676 MDNS
.notifyAPChange();
678 changeMode
= WIFI_OFF
;
683 dnsServer
.processNextRequest();
684 #if defined(PLATFORM_ESP8266)
687 // When in STA mode, a small delay reduces power use from 90mA to 30mA when idle
688 // In AP mode, it doesn't seem to make a measurable difference, but does not hurt
689 if (!Update
.isRunning())
696 ipAddress
.fromString(wifi_ap_address
);
698 #ifdef AUTO_WIFI_ON_INTERVAL
699 return AUTO_WIFI_ON_INTERVAL
* 1000;
701 return DURATION_NEVER
;
707 if (connectionState
== wifiUpdate
|| connectionState
> FAILURE_STATES
)
711 return DURATION_IMMEDIATELY
;
714 return DURATION_IGNORE
;
722 return DURATION_IMMEDIATELY
;
725 #if defined(TARGET_TX) && defined(AUTO_WIFI_ON_INTERVAL)
726 //if webupdate was requested before or AUTO_WIFI_ON_INTERVAL has been elapsed but uart is not detected
727 //start webupdate, there might be wrong configuration flashed.
728 if(webserverPreventAutoStart
== false && connectionState
< wifiUpdate
&& !wifiStarted
){
729 DBGLN("No CRSF ever detected, starting WiFi");
730 connectionState
= wifiUpdate
;
731 return DURATION_IMMEDIATELY
;
733 #elif defined(TARGET_RX) && defined(AUTO_WIFI_ON_INTERVAL)
734 if (!webserverPreventAutoStart
&& (connectionState
== disconnected
))
736 static bool pastAutoInterval
= false;
737 // If InBindingMode then wait at least 60 seconds before going into wifi,
738 // regardless of if AUTO_WIFI_ON_INTERVAL is set to less
739 if (!InBindingMode
|| AUTO_WIFI_ON_INTERVAL
>= 60 || pastAutoInterval
)
741 // No need to ExitBindingMode(), the radio is about to be stopped. Need
742 // to change this before the mode change event so the LED is updated
743 InBindingMode
= false;
744 connectionState
= wifiUpdate
;
745 return DURATION_IMMEDIATELY
;
747 pastAutoInterval
= true;
748 return (60 - AUTO_WIFI_ON_INTERVAL
) * 1000;
751 return DURATION_NEVER
;
754 device_t WIFI_device
= {
755 .initialize
= wifiOff
,