diff --git a/interface/src/app/main/Sensors.tsx b/interface/src/app/main/Sensors.tsx index bbb8fd636..891b41954 100644 --- a/interface/src/app/main/Sensors.tsx +++ b/interface/src/app/main/Sensors.tsx @@ -54,7 +54,7 @@ const MS_PER_SECOND = 1000; const MS_PER_MINUTE = 60 * MS_PER_SECOND; const MS_PER_HOUR = 60 * MS_PER_MINUTE; const MS_PER_DAY = 24 * MS_PER_HOUR; -const DEFAULT_GPIO = 21; // Safe GPIO for all platforms +const DEFAULT_GPIO = -1; // not set const MIN_TEMP_ID = -100; const MAX_TEMP_ID = 100; const GPIO_25 = 25; @@ -134,6 +134,7 @@ const Sensors = () => { ts: [], as: [], analog_enabled: false, + valid_gpio_list: [], platform: 'ESP32' } }); @@ -573,10 +574,10 @@ const Sensors = () => { onSave={onAnalogDialogSave} creating={creating} selectedItem={selectedAnalogSensor} + analogGPIOList={sensorData.valid_gpio_list} validator={analogSensorItemValidation( sensorData.as, selectedAnalogSensor, - creating, sensorData.platform )} /> diff --git a/interface/src/app/main/SensorsAnalogDialog.tsx b/interface/src/app/main/SensorsAnalogDialog.tsx index ee5087d12..6473dec2a 100644 --- a/interface/src/app/main/SensorsAnalogDialog.tsx +++ b/interface/src/app/main/SensorsAnalogDialog.tsx @@ -35,6 +35,7 @@ interface DashboardSensorsAnalogDialogProps { onSave: (as: AnalogSensor) => void; creating: boolean; selectedItem: AnalogSensor; + analogGPIOList: number[]; validator: Schema; } @@ -44,6 +45,7 @@ const SensorsAnalogDialog = ({ onSave, creating, selectedItem, + analogGPIOList, validator }: DashboardSensorsAnalogDialogProps) => { const { LL } = useI18nContext(); @@ -156,28 +158,23 @@ const SensorsAnalogDialog = ({ {dialogTitle} + + {analogGPIOList?.map((gpio: number) => ( + + {gpio} + + ))} + - - {creating && ( - - - {LL.WARN_GPIO()} - - - )} - - {analogTypeMenuItems} @@ -207,6 +205,7 @@ const SensorsAnalogDialog = ({ sx={{ width: '15ch' }} select onChange={updateFormValue} + disabled={editItem.s} > {uomMenuItems} @@ -222,6 +221,7 @@ const SensorsAnalogDialog = ({ sx={{ width: '11ch' }} variant="outlined" onChange={updateFormValue} + disabled={editItem.s} slotProps={{ input: { startAdornment: ( @@ -243,6 +243,7 @@ const SensorsAnalogDialog = ({ type="number" variant="outlined" onChange={updateFormValue} + disabled={editItem.s} slotProps={{ input: { startAdornment: ( @@ -264,6 +265,7 @@ const SensorsAnalogDialog = ({ sx={{ width: '11ch' }} variant="outlined" onChange={updateFormValue} + disabled={editItem.s} slotProps={{ htmlInput: { step: '0.001' } }} @@ -280,6 +282,7 @@ const SensorsAnalogDialog = ({ sx={{ width: '11ch' }} variant="outlined" onChange={updateFormValue} + disabled={editItem.s} /> )} @@ -293,6 +296,7 @@ const SensorsAnalogDialog = ({ type="number" variant="outlined" onChange={updateFormValue} + disabled={editItem.s} slotProps={{ htmlInput: { step: '0.001' } }} @@ -309,6 +313,7 @@ const SensorsAnalogDialog = ({ type="number" variant="outlined" onChange={updateFormValue} + disabled={editItem.s} slotProps={{ htmlInput: { min: '0', max: '255', step: '1' } }} @@ -325,6 +330,7 @@ const SensorsAnalogDialog = ({ select variant="outlined" onChange={updateFormValue} + disabled={editItem.s} > {LL.OFF()} {LL.ON()} @@ -338,6 +344,7 @@ const SensorsAnalogDialog = ({ sx={{ width: '15ch' }} select onChange={updateFormValue} + disabled={editItem.s} > {LL.ACTIVEHIGH()} {LL.ACTIVELOW()} @@ -351,6 +358,7 @@ const SensorsAnalogDialog = ({ value={editItem.u} select onChange={updateFormValue} + disabled={editItem.s} > {LL.UNCHANGED()} @@ -374,6 +382,7 @@ const SensorsAnalogDialog = ({ variant="outlined" sx={{ width: '11ch' }} onChange={updateFormValue} + disabled={editItem.s} slotProps={{ input: { startAdornment: ( @@ -393,6 +402,7 @@ const SensorsAnalogDialog = ({ sx={{ width: '11ch' }} variant="outlined" onChange={updateFormValue} + disabled={editItem.s} slotProps={{ input: { startAdornment: ( @@ -415,6 +425,7 @@ const SensorsAnalogDialog = ({ sx={{ width: '11ch' }} select onChange={updateFormValue} + disabled={editItem.s} > {LL.ACTIVEHIGH()} {LL.ACTIVELOW()} @@ -429,6 +440,7 @@ const SensorsAnalogDialog = ({ sx={{ width: '15ch' }} variant="outlined" onChange={updateFormValue} + disabled={editItem.s} slotProps={{ input: { startAdornment: ( @@ -442,6 +454,24 @@ const SensorsAnalogDialog = ({ )} + {fieldErrors && Object.keys(fieldErrors).length > 0 && ( + + {Object.values(fieldErrors).map((errArr, idx) => + Array.isArray(errArr) + ? errArr.map((err, j) => ( + + {err.message} + + )) + : null + )} + + )} {editItem.s && ( diff --git a/interface/src/app/main/types.ts b/interface/src/app/main/types.ts index 26dd876c0..280dbc1c3 100644 --- a/interface/src/app/main/types.ts +++ b/interface/src/app/main/types.ts @@ -111,6 +111,7 @@ export interface SensorData { ts: TemperatureSensor[]; as: AnalogSensor[]; analog_enabled: boolean; + valid_gpio_list: number[]; platform: string; } diff --git a/interface/src/app/main/validators.ts b/interface/src/app/main/validators.ts index ad4d6f95b..7ededce0c 100644 --- a/interface/src/app/main/validators.ts +++ b/interface/src/app/main/validators.ts @@ -45,121 +45,15 @@ const VALIDATION_LIMITS = { HEX_BASE: 16 } as const; -// Helper to create GPIO validator from invalid ranges -const createGPIOValidator = ( - invalidRanges: Array, - maxValue: number -) => ({ - validator( - _rule: InternalRuleItem, - value: number, - callback: (error?: string) => void - ) { - if (!value) { - callback(); - return; - } - - if (value < 0 || value > maxValue) { - callback(ERROR_MESSAGES.GPIO_INVALID); - return; - } - - for (const range of invalidRanges) { - if (typeof range === 'number') { - if (value === range) { - callback(ERROR_MESSAGES.GPIO_INVALID); - return; - } - } else { - const [start, end] = range; - if (value >= start && value <= end) { - callback(ERROR_MESSAGES.GPIO_INVALID); - return; - } - } - } - - callback(); - } -}); - -export const GPIO_VALIDATOR = createGPIOValidator( - [[6, 11], 1, 20, 24, [28, 31]], - 40 -); - -export const GPIO_VALIDATORC3 = createGPIOValidator([[11, 19]], 21); - -export const GPIO_VALIDATORS2 = createGPIOValidator( - [ - [19, 20], - [22, 32] - ], - 40 -); - -export const GPIO_VALIDATORS3 = createGPIOValidator( - [ - [19, 20], - [22, 37], - [39, 42] - ], - 48 -); - -const GPIO_FIELD_NAMES = [ - 'led_gpio', - 'dallas_gpio', - 'pbutton_gpio', - 'tx_gpio', - 'rx_gpio' -] as const; - type ValidationRules = Array<{ required?: boolean; message?: string; [key: string]: unknown; }>; -const createGPIOValidations = ( - validator: typeof GPIO_VALIDATOR -): Record => - GPIO_FIELD_NAMES.reduce( - (acc, field) => { - const fieldName = field.replace('_gpio', '').toUpperCase(); - acc[field] = [ - { required: true, message: `${fieldName} GPIO is required` }, - validator - ]; - return acc; - }, - {} as Record - ); - -const PLATFORM_VALIDATORS = { - ESP32: GPIO_VALIDATOR, - ESP32C3: GPIO_VALIDATORC3, - ESP32S2: GPIO_VALIDATORS2, - ESP32S3: GPIO_VALIDATORS3 -} as const; - export const createSettingsValidator = (settings: Settings) => { const schema: Record = {}; - // Add GPIO validations for CUSTOM board profiles - if ( - settings.board_profile === 'CUSTOM' && - settings.platform in PLATFORM_VALIDATORS - ) { - Object.assign( - schema, - createGPIOValidations( - PLATFORM_VALIDATORS[settings.platform as keyof typeof PLATFORM_VALIDATORS] - ) - ); - } - // Syslog validations if (settings.syslog_enabled) { schema.syslog_host = [ @@ -401,52 +295,29 @@ export const temperatureSensorItemValidation = ( n: [NAME_PATTERN, uniqueTemperatureNameValidator(sensors, sensor.o_n)] }); -export const isGPIOUniqueValidator = (sensors: AnalogSensor[]) => ({ - validator( - _rule: InternalRuleItem, - gpio: number, - callback: (error?: string) => void - ) { - if (sensors.some((as) => as.g === gpio)) { - callback(ERROR_MESSAGES.GPIO_DUPLICATE); - return; - } - callback(); - } -}); - export const uniqueAnalogNameValidator = ( sensors: AnalogSensor[], o_name?: string ) => createUniqueFieldNameValidator(sensors, (s) => s.n, o_name); -const getPlatformGPIOValidator = (platform: string) => { - switch (platform) { - case 'ESP32S3': - return GPIO_VALIDATORS3; - case 'ESP32S2': - return GPIO_VALIDATORS2; - case 'ESP32C3': - return GPIO_VALIDATORC3; - default: - return GPIO_VALIDATOR; - } -}; - export const analogSensorItemValidation = ( sensors: AnalogSensor[], - sensor: AnalogSensor, - creating: boolean, - platform: string + sensor: AnalogSensor ) => { - const gpioValidator = getPlatformGPIOValidator(platform); - return new Schema({ - n: [NAME_PATTERN, uniqueAnalogNameValidator(sensors, sensor.o_n)], + // name is required and must be unique + n: [ + { required: true, message: 'Name is required' }, + NAME_PATTERN, + uniqueAnalogNameValidator(sensors, sensor.o_n) + ], g: [ - { required: true, message: 'GPIO is required' }, - gpioValidator, - ...(creating ? [isGPIOUniqueValidator(sensors)] : []) + { + required: true, + type: 'number', + min: 1, + message: 'GPIO is required' + } ] }); }; diff --git a/interface/src/i18n/cz/index.ts b/interface/src/i18n/cz/index.ts index 61b156e6c..704ba83cc 100644 --- a/interface/src/i18n/cz/index.ts +++ b/interface/src/i18n/cz/index.ts @@ -60,7 +60,6 @@ const cz: Translation = { DUTY_CYCLE: 'Pracovní cyklus', UNIT: 'Jednotka', STARTVALUE: 'Počáteční hodnota', - WARN_GPIO: 'Upozornění: buďte opatrní při přiřazování GPIO!', EDIT: 'Upravit', SENSOR: 'Senzor', TEMP_SENSOR: 'Teplotní senzor', diff --git a/interface/src/i18n/de/index.ts b/interface/src/i18n/de/index.ts index cb481cb0e..1e578fc24 100644 --- a/interface/src/i18n/de/index.ts +++ b/interface/src/i18n/de/index.ts @@ -60,7 +60,6 @@ const de: Translation = { DUTY_CYCLE: 'Arbeitszyklus', UNIT: 'Maßeinheit', STARTVALUE: 'Startwert', - WARN_GPIO: 'Warnung: Vorsicht bei der korrekten Wahl des GPIO!', EDIT: 'Editiere', SENSOR: 'Sensor', TEMP_SENSOR: 'Temperatursensor', diff --git a/interface/src/i18n/en/index.ts b/interface/src/i18n/en/index.ts index 81f07294d..38a6e3185 100644 --- a/interface/src/i18n/en/index.ts +++ b/interface/src/i18n/en/index.ts @@ -60,7 +60,6 @@ const en: Translation = { DUTY_CYCLE: 'Duty Cycle', UNIT: 'UoM', STARTVALUE: 'Start Value', - WARN_GPIO: 'Warning: be careful when assigning a GPIO!', EDIT: 'Edit', SENSOR: 'Sensor', TEMP_SENSOR: 'Temperature Sensor', diff --git a/interface/src/i18n/fr/index.ts b/interface/src/i18n/fr/index.ts index cda12b9f0..cfbbaa81d 100644 --- a/interface/src/i18n/fr/index.ts +++ b/interface/src/i18n/fr/index.ts @@ -60,7 +60,6 @@ const fr: Translation = { DUTY_CYCLE: 'Cycle de fonctionnement', UNIT: 'Unité', STARTVALUE: 'Valeur de départ', - WARN_GPIO: 'Attention: soyez vigilant en choisissant un GPIO!', EDIT: 'Éditer', SENSOR: 'Capteur', TEMP_SENSOR: 'Capteur de température', diff --git a/interface/src/i18n/it/index.ts b/interface/src/i18n/it/index.ts index 44cf4f9b6..3dba434f6 100644 --- a/interface/src/i18n/it/index.ts +++ b/interface/src/i18n/it/index.ts @@ -60,7 +60,6 @@ const it: Translation = { DUTY_CYCLE: 'Ciclo di lavoro', UNIT: 'UoM', STARTVALUE: 'Valore di partenza', - WARN_GPIO: 'Avvertimento: prestare attenzione quando si assegna un GPIO!', EDIT: 'Modifica', SENSOR: 'Sensore', TEMP_SENSOR: 'Sensore Temperatura', diff --git a/interface/src/i18n/nl/index.ts b/interface/src/i18n/nl/index.ts index affdddfe2..579202eb6 100644 --- a/interface/src/i18n/nl/index.ts +++ b/interface/src/i18n/nl/index.ts @@ -60,7 +60,6 @@ const nl: Translation = { DUTY_CYCLE: 'Duty Cycle', UNIT: 'UoM', STARTVALUE: 'Startwaarde', - WARN_GPIO: 'Waarschuwing: let op met het koppelen van de juiste GPIO pin!', EDIT: 'Wijzigen', SENSOR: 'Sensor', TEMP_SENSOR: 'Temperatuur sensor', diff --git a/interface/src/i18n/no/index.ts b/interface/src/i18n/no/index.ts index 0c1e88133..a2fbbaa6c 100644 --- a/interface/src/i18n/no/index.ts +++ b/interface/src/i18n/no/index.ts @@ -60,7 +60,6 @@ const no: Translation = { DUTY_CYCLE: 'Duty Cycle', UNIT: 'UoM', STARTVALUE: 'Startverdi', - WARN_GPIO: 'Advarsel: vær forsiktig ved aktivering av GPIO!', EDIT: 'Endre', SENSOR: 'Sensor', TEMP_SENSOR: 'Temperatursensor', diff --git a/interface/src/i18n/pl/index.ts b/interface/src/i18n/pl/index.ts index bbc90b380..6dd874a32 100644 --- a/interface/src/i18n/pl/index.ts +++ b/interface/src/i18n/pl/index.ts @@ -60,7 +60,6 @@ const pl: BaseTranslation = { DUTY_CYCLE: 'Wypełnienie', UNIT: 'J.m.', STARTVALUE: 'Wartość początkowa', - WARN_GPIO: 'Uwaga! Zachowaj ostrożność przypisując GPIO do urządzenia!', EDIT: 'Edycja', SENSOR: '{{c|ustawienia c||ustawień c|}}zujnika', TEMP_SENSOR: 'czujnika temperatury', diff --git a/interface/src/i18n/sk/index.ts b/interface/src/i18n/sk/index.ts index aa22a83cb..52e6f9c75 100644 --- a/interface/src/i18n/sk/index.ts +++ b/interface/src/i18n/sk/index.ts @@ -60,7 +60,6 @@ const sk: Translation = { DUTY_CYCLE: 'Pracovný cyklus', UNIT: 'UoM', STARTVALUE: 'Počiatočná hodnota', - WARN_GPIO: 'Upozornenie: Buďte opatrní pri priraďovaní GPIO!', EDIT: 'Editovať', SENSOR: 'Snímač', TEMP_SENSOR: 'Snímač teploty', diff --git a/interface/src/i18n/sv/index.ts b/interface/src/i18n/sv/index.ts index 9a07450b8..644484269 100644 --- a/interface/src/i18n/sv/index.ts +++ b/interface/src/i18n/sv/index.ts @@ -60,7 +60,6 @@ const sv: Translation = { DUTY_CYCLE: 'Pulskvot', UNIT: 'Måttenhet', STARTVALUE: 'Startvärde', - WARN_GPIO: 'Varning: Var försiktig vid aktivering av GPIO!', EDIT: 'Ändra', SENSOR: 'Sensor', TEMP_SENSOR: 'Temperatursensor', diff --git a/interface/src/i18n/tr/index.ts b/interface/src/i18n/tr/index.ts index e212ea274..79824decd 100644 --- a/interface/src/i18n/tr/index.ts +++ b/interface/src/i18n/tr/index.ts @@ -60,7 +60,6 @@ const tr: Translation = { DUTY_CYCLE: 'Görev Çevrimi', UNIT: 'ÖB', STARTVALUE: 'Başlangıç değeri', - WARN_GPIO: 'Uyarı: bir GPIO atarken dikkatli olun!', EDIT: 'Değiştir', SENSOR: 'Sensör', TEMP_SENSOR: 'Sıcaklık Sensörü', diff --git a/mock-api/restServer.ts b/mock-api/restServer.ts index eb8f18acb..ce7484bea 100644 --- a/mock-api/restServer.ts +++ b/mock-api/restServer.ts @@ -1046,7 +1046,8 @@ const emsesp_sensordata = { s: true } ], - analog_enabled: true + analog_enabled: true, + valid_gpio_list: [6, 11, 20, 24, 28, 31, 1, 40] }; const activity = { diff --git a/src/core/system.cpp b/src/core/system.cpp index ac0ce94ef..4064ca4cd 100644 --- a/src/core/system.cpp +++ b/src/core/system.cpp @@ -2320,4 +2320,41 @@ uint8_t System::systemStatus() { return systemStatus_; } +// return the list of valid GPIOs +std::vector System::valid_gpio_list() const { + // get free gpios based on board type +#if CONFIG_IDF_TARGET_ESP32C3 + std::vector valid_gpios = {11, 19, 21}; +#elif CONFIG_IDF_TARGET_ESP32S2 + std::vector valid_gpios = {19, 20, 22, 32, 40}; +#elif CONFIG_IDF_TARGET_ESP32S3 + std::vector valid_gpios = {19, 20, 22, 37, 39, 42, 48}; +#elif CONFIG_IDF_TARGET_ESP32 + std::vector valid_gpios = {6, 11, 20, 24, 28, 31, 1, 40}; +#else + std::vector valid_gpios = {}; +#endif + + // filter out GPIOs already used in application settings + for (const auto & gpio : valid_gpios) { + if (gpio == pbutton_gpio_ || gpio == led_gpio_ || gpio == dallas_gpio_ || gpio == rx_gpio_ || gpio == tx_gpio_) { + valid_gpios.erase(std::remove(valid_gpios.begin(), valid_gpios.end(), gpio), valid_gpios.end()); + } + } + + // filter out GPIOs already used in analog sensors, if enabled + if (analog_enabled_) { + for (const auto & sensor : EMSESP::analogsensor_.sensors()) { + if (std::find(valid_gpios.begin(), valid_gpios.end(), sensor.gpio()) != valid_gpios.end()) { + valid_gpios.erase(std::find(valid_gpios.begin(), valid_gpios.end(), sensor.gpio())); + } + } + } + + // sort the list of valid GPIOs + std::sort(valid_gpios.begin(), valid_gpios.end()); + + return valid_gpios; +} + } // namespace emsesp diff --git a/src/core/system.h b/src/core/system.h index 01bf97f88..7e4e08f32 100644 --- a/src/core/system.h +++ b/src/core/system.h @@ -137,6 +137,8 @@ class System { void systemStatus(uint8_t status_code); uint8_t systemStatus(); + std::vector valid_gpio_list() const; + static void extractSettings(const char * filename, const char * section, JsonObject output); static bool saveSettings(const char * filename, const char * section, JsonObject input); diff --git a/src/web/WebDataService.cpp b/src/web/WebDataService.cpp index dc5005bd6..4b78399f4 100644 --- a/src/web/WebDataService.cpp +++ b/src/web/WebDataService.cpp @@ -157,6 +157,11 @@ void WebDataService::sensor_data(AsyncWebServerRequest * request) { root["analog_enabled"] = EMSESP::analog_enabled(); root["platform"] = EMSESP_PLATFORM; + JsonArray valid_gpio_list = root["valid_gpio_list"].to(); + for (const auto & gpio : EMSESP::system_.valid_gpio_list()) { + valid_gpio_list.add(gpio); + } + response->setLength(); request->send(response); }