diff --git a/components/connect/connect.c b/components/connect/connect.c index 5214aff97..59b401084 100644 --- a/components/connect/connect.c +++ b/components/connect/connect.c @@ -15,6 +15,9 @@ #include "connect.h" #include "main.h" +// Maximum number of access points to scan +#define MAX_AP_COUNT 20 + #if CONFIG_ESP_WPA3_SAE_PWE_HUNT_AND_PECK #define ESP_WIFI_SAE_MODE WPA3_SAE_PWE_HUNT_AND_PECK #define EXAMPLE_H2E_IDENTIFIER "" @@ -51,6 +54,34 @@ static EventGroupHandle_t s_wifi_event_group; static const char * TAG = "wifi_station"; +// Function to scan for available WiFi networks +esp_err_t wifi_scan(wifi_ap_record_simple_t *ap_records, uint16_t *ap_count) { + wifi_scan_config_t scan_config = { + .ssid = 0, + .bssid = 0, + .channel = 0, + .show_hidden = false + }; + + // Start WiFi scan + ESP_ERROR_CHECK(esp_wifi_scan_start(&scan_config, true)); + + // Get scan results + uint16_t number = MAX_AP_COUNT; + wifi_ap_record_t ap_info[MAX_AP_COUNT]; + ESP_ERROR_CHECK(esp_wifi_scan_get_ap_records(&number, ap_info)); + + // Store results in simplified structure + *ap_count = number; + for (int i = 0; i < number; i++) { + memcpy(ap_records[i].ssid, ap_info[i].ssid, sizeof(ap_records[i].ssid)); + ap_records[i].rssi = ap_info[i].rssi; + ap_records[i].authmode = ap_info[i].authmode; + } + + return ESP_OK; +} + static int s_retry_num = 0; static char * _ip_addr_str; diff --git a/components/connect/include/connect.h b/components/connect/include/connect.h index 800aafa61..a753108ba 100644 --- a/components/connect/include/connect.h +++ b/components/connect/include/connect.h @@ -5,6 +5,14 @@ #include #include "freertos/event_groups.h" +#include "esp_wifi_types.h" + +// Structure to hold WiFi scan results +typedef struct { + char ssid[33]; // 32 chars + null terminator + int8_t rssi; + wifi_auth_mode_t authmode; +} wifi_ap_record_simple_t; #define WIFI_SSID CONFIG_ESP_WIFI_SSID #define WIFI_PASS CONFIG_ESP_WIFI_PASSWORD @@ -32,3 +40,4 @@ void wifi_softap_off(void); void wifi_init(const char * wifi_ssid, const char * wifi_pass, const char * hostname, char * ip_addr_str); EventBits_t wifi_connect(void); void generate_ssid(char * ssid); +esp_err_t wifi_scan(wifi_ap_record_simple_t *ap_records, uint16_t *ap_count); diff --git a/main/http_server/axe-os/src/app/app.module.ts b/main/http_server/axe-os/src/app/app.module.ts index e5714317e..2f8c73961 100644 --- a/main/http_server/axe-os/src/app/app.module.ts +++ b/main/http_server/axe-os/src/app/app.module.ts @@ -28,6 +28,9 @@ import { HashSuffixPipe } from './pipes/hash-suffix.pipe'; import { PrimeNGModule } from './prime-ng.module'; import { MessageModule } from 'primeng/message'; import { TooltipModule } from 'primeng/tooltip'; +import { DialogModule } from 'primeng/dialog'; +import { DynamicDialogModule, DialogService as PrimeDialogService } from 'primeng/dynamicdialog'; +import { DialogService, DialogListComponent } from './services/dialog.service'; const components = [ AppComponent, @@ -52,7 +55,8 @@ const components = [ HashSuffixPipe, ThemeConfigComponent, DesignComponent, - PoolComponent + PoolComponent, + DialogListComponent ], imports: [ BrowserModule, @@ -68,10 +72,14 @@ const components = [ PrimeNGModule, AppLayoutModule, MessageModule, - TooltipModule + TooltipModule, + DialogModule, + DynamicDialogModule ], providers: [ { provide: LocationStrategy, useClass: HashLocationStrategy }, + DialogService, + PrimeDialogService ], bootstrap: [AppComponent] }) diff --git a/main/http_server/axe-os/src/app/components/network-edit/network.edit.component.html b/main/http_server/axe-os/src/app/components/network-edit/network.edit.component.html index 647643aa4..b635ef9e4 100644 --- a/main/http_server/axe-os/src/app/components/network-edit/network.edit.component.html +++ b/main/http_server/axe-os/src/app/components/network-edit/network.edit.component.html @@ -9,8 +9,9 @@
-
+
+
diff --git a/main/http_server/axe-os/src/app/components/network-edit/network.edit.component.ts b/main/http_server/axe-os/src/app/components/network-edit/network.edit.component.ts index ff07b2380..586db1483 100644 --- a/main/http_server/axe-os/src/app/components/network-edit/network.edit.component.ts +++ b/main/http_server/axe-os/src/app/components/network-edit/network.edit.component.ts @@ -1,11 +1,18 @@ -import { HttpErrorResponse } from '@angular/common/http'; +import { HttpClient, HttpErrorResponse } from '@angular/common/http'; import { Component, Input, OnInit } from '@angular/core'; import { FormBuilder, FormGroup, Validators } from '@angular/forms'; import { ToastrService } from 'ngx-toastr'; -import { startWith } from 'rxjs'; +import { finalize } from 'rxjs/operators'; +import { DialogService } from 'src/app/services/dialog.service'; import { LoadingService } from 'src/app/services/loading.service'; import { SystemService } from 'src/app/services/system.service'; +interface WifiNetwork { + ssid: string; + rssi: number; + authmode: number; +} + @Component({ selector: 'app-network-edit', templateUrl: './network.edit.component.html', @@ -15,6 +22,7 @@ export class NetworkEditComponent implements OnInit { public form!: FormGroup; public savedChanges: boolean = false; + public scanning: boolean = false; @Input() uri = ''; @@ -23,7 +31,9 @@ export class NetworkEditComponent implements OnInit { private systemService: SystemService, private toastr: ToastrService, private toastrService: ToastrService, - private loadingService: LoadingService + private loadingService: LoadingService, + private http: HttpClient, + private dialogService: DialogService ) { } @@ -71,6 +81,52 @@ export class NetworkEditComponent implements OnInit { this.showWifiPassword = !this.showWifiPassword; } + public scanWifi() { + this.scanning = true; + this.http.get<{networks: WifiNetwork[]}>('/api/system/wifi/scan') + .pipe( + finalize(() => this.scanning = false) + ) + .subscribe({ + next: (response) => { + // Sort networks by signal strength (highest first) + const networks = response.networks.sort((a, b) => b.rssi - a.rssi); + + // filter out poor wifi connections + const poorNetworks = networks.filter(network => network.rssi >= -80); + + // Remove duplicate Network Names and show highest signal strength only + const uniqueNetworks = poorNetworks.reduce((acc, network) => { + if (!acc[network.ssid] || acc[network.ssid].rssi < network.rssi) { + acc[network.ssid] = network; + } + return acc; + }, {} as { [key: string]: WifiNetwork }); + + // Convert the object back to an array + const filteredNetworks = Object.values(uniqueNetworks); + + // Create dialog data + const dialogData = filteredNetworks.map(n => ({ + label: `${n.ssid} (${n.rssi}dBm)`, + value: n.ssid + })); + + // Show dialog with network list + this.dialogService.open('Select WiFi Network', dialogData) + .subscribe((selectedSsid: string) => { + if (selectedSsid) { + this.form.patchValue({ ssid: selectedSsid }); + this.form.markAsDirty(); + } + }); + }, + error: (err) => { + this.toastr.error('Failed to scan WiFi networks', 'Error'); + } + }); + } + public restart() { this.systemService.restart() .pipe(this.loadingService.lockUIUntilComplete()) diff --git a/main/http_server/axe-os/src/app/services/dialog.service.ts b/main/http_server/axe-os/src/app/services/dialog.service.ts new file mode 100644 index 000000000..9188cace3 --- /dev/null +++ b/main/http_server/axe-os/src/app/services/dialog.service.ts @@ -0,0 +1,58 @@ +import { Component, Injectable } from '@angular/core'; +import { Observable, Subject } from 'rxjs'; +import { DialogService as PrimeDialogService, DynamicDialogConfig } from 'primeng/dynamicdialog'; + +interface DialogOption { + label: string; + value: string; +} + +@Injectable({ + providedIn: 'root' +}) +export class DialogService { + constructor(private primeDialogService: PrimeDialogService) {} + + open(title: string, options: DialogOption[]): Observable { + const result = new Subject(); + + const ref = this.primeDialogService.open(DialogListComponent, { + header: title, + width: '400px', + data: { + options: options, + onSelect: (value: string) => { + result.next(value); + ref.close(); + } + } + }); + + ref.onClose.subscribe(() => { + result.complete(); + }); + + return result.asObservable(); + } +} + +@Component({ + template: ` + +
+ +
+ ` +}) +export class DialogListComponent { + constructor(public config: DynamicDialogConfig) {} +} diff --git a/main/http_server/http_server.c b/main/http_server/http_server.c index fba3d4461..47a7685b4 100644 --- a/main/http_server/http_server.c +++ b/main/http_server/http_server.c @@ -15,6 +15,7 @@ #include "global_state.h" #include "nvs_config.h" #include "vcore.h" +#include "connect.h" #include #include #include @@ -35,6 +36,41 @@ static const char * TAG = "http_server"; static const char * CORS_TAG = "CORS"; +/* Handler for WiFi scan endpoint */ +static esp_err_t GET_wifi_scan(httpd_req_t *req) +{ + httpd_resp_set_type(req, "application/json"); + + wifi_ap_record_simple_t ap_records[20]; + uint16_t ap_count = 0; + + esp_err_t err = wifi_scan(ap_records, &ap_count); + if (err != ESP_OK) { + httpd_resp_send_err(req, HTTPD_500_INTERNAL_SERVER_ERROR, "WiFi scan failed"); + return ESP_OK; + } + + cJSON *root = cJSON_CreateObject(); + cJSON *networks = cJSON_CreateArray(); + + for (int i = 0; i < ap_count; i++) { + cJSON *network = cJSON_CreateObject(); + cJSON_AddStringToObject(network, "ssid", (char *)ap_records[i].ssid); + cJSON_AddNumberToObject(network, "rssi", ap_records[i].rssi); + cJSON_AddNumberToObject(network, "authmode", ap_records[i].authmode); + cJSON_AddItemToArray(networks, network); + } + + cJSON_AddItemToObject(root, "networks", networks); + + const char *response = cJSON_Print(root); + httpd_resp_sendstr(req, response); + + free((void *)response); + cJSON_Delete(root); + return ESP_OK; +} + static GlobalState * GLOBAL_STATE; static httpd_handle_t server = NULL; QueueHandle_t log_queue = NULL; @@ -857,6 +893,15 @@ esp_err_t start_rest_server(void * pvParameters) }; httpd_register_uri_handler(server, &system_info_get_uri); + /* URI handler for WiFi scan */ + httpd_uri_t wifi_scan_get_uri = { + .uri = "/api/system/wifi/scan", + .method = HTTP_GET, + .handler = GET_wifi_scan, + .user_ctx = rest_context + }; + httpd_register_uri_handler(server, &wifi_scan_get_uri); + httpd_uri_t swarm_options_uri = { .uri = "/api/swarm", .method = HTTP_OPTIONS,