Set device tags after FUOTA complete.

This commit is contained in:
Orne Brocaar 2025-03-11 09:36:55 +00:00
parent bbdf2dd781
commit 98ba2f3198
10 changed files with 289 additions and 203 deletions

View File

@ -156,6 +156,9 @@ message FuotaDeployment {
// Payload. // Payload.
// The FUOTA payload to send. // The FUOTA payload to send.
bytes payload = 21; bytes payload = 21;
// Set device tags on complete.
map<string, string> on_complete_set_device_tags = 22;
} }
message FuotaDeploymentListItem { message FuotaDeploymentListItem {

View File

@ -156,6 +156,9 @@ message FuotaDeployment {
// Payload. // Payload.
// The FUOTA payload to send. // The FUOTA payload to send.
bytes payload = 21; bytes payload = 21;
// Set device tags on complete.
map<string, string> on_complete_set_device_tags = 22;
} }
message FuotaDeploymentListItem { message FuotaDeploymentListItem {

View File

@ -23,7 +23,8 @@ create table fuota_deployment (
fragmentation_block_ack_delay smallint not null, fragmentation_block_ack_delay smallint not null,
fragmentation_descriptor bytea not null, fragmentation_descriptor bytea not null,
request_fragmentation_session_status varchar(20) not null, request_fragmentation_session_status varchar(20) not null,
payload bytea not null payload bytea not null,
on_complete_set_device_tags jsonb not null
); );
create table fuota_deployment_device ( create table fuota_deployment_device (

View File

@ -23,7 +23,8 @@ create table fuota_deployment (
fragmentation_block_ack_delay smallint not null, fragmentation_block_ack_delay smallint not null,
fragmentation_descriptor blob not null, fragmentation_descriptor blob not null,
request_fragmentation_session_status varchar(20) not null, request_fragmentation_session_status varchar(20) not null,
payload blob not null payload blob not null,
on_complete_set_device_tags text not null
); );
create table fuota_deployment_device ( create table fuota_deployment_device (

View File

@ -77,6 +77,9 @@ impl FuotaService for Fuota {
.request_fragmentation_session_status() .request_fragmentation_session_status()
.from_proto(), .from_proto(),
payload: req_dp.payload.clone(), payload: req_dp.payload.clone(),
on_complete_set_device_tags: fields::KeyValue::new(
req_dp.on_complete_set_device_tags.clone(),
),
..Default::default() ..Default::default()
}; };
if req_dp.calculate_fragmentation_fragment_size { if req_dp.calculate_fragmentation_fragment_size {
@ -152,6 +155,7 @@ impl FuotaService for Fuota {
payload: dp.payload.clone(), payload: dp.payload.clone(),
calculate_multicast_timeout: false, calculate_multicast_timeout: false,
calculate_fragmentation_fragment_size: false, calculate_fragmentation_fragment_size: false,
on_complete_set_device_tags: dp.on_complete_set_device_tags.into_hashmap(),
}), }),
created_at: Some(helpers::datetime_to_prost_timestamp(&dp.created_at)), created_at: Some(helpers::datetime_to_prost_timestamp(&dp.created_at)),
updated_at: Some(helpers::datetime_to_prost_timestamp(&dp.updated_at)), updated_at: Some(helpers::datetime_to_prost_timestamp(&dp.updated_at)),
@ -227,6 +231,9 @@ impl FuotaService for Fuota {
.request_fragmentation_session_status() .request_fragmentation_session_status()
.from_proto(), .from_proto(),
payload: req_dp.payload.clone(), payload: req_dp.payload.clone(),
on_complete_set_device_tags: fields::KeyValue::new(
req_dp.on_complete_set_device_tags.clone(),
),
..Default::default() ..Default::default()
}; };
if req_dp.calculate_fragmentation_fragment_size { if req_dp.calculate_fragmentation_fragment_size {

View File

@ -1,3 +1,4 @@
use std::ops::DerefMut;
use std::time::Duration; use std::time::Duration;
use anyhow::Result; use anyhow::Result;
@ -11,7 +12,7 @@ use crate::config;
use crate::downlink; use crate::downlink;
use crate::gpstime::ToGpsTime; use crate::gpstime::ToGpsTime;
use crate::storage::fields::{FuotaJob, RequestFragmentationSessionStatus}; use crate::storage::fields::{FuotaJob, RequestFragmentationSessionStatus};
use crate::storage::{device_keys, device_profile, device_queue, fuota, multicast}; use crate::storage::{device, device_keys, device_profile, device_queue, fuota, multicast};
pub struct Flow { pub struct Flow {
scheduler_interval: Duration, scheduler_interval: Duration,
@ -569,7 +570,7 @@ impl Flow {
return Ok(None); return Ok(None);
} }
info!("Complete FUOTA deployment"); info!("Completing FUOTA deployment");
self.job.attempt_count += 1; self.job.attempt_count += 1;
if self.fuota_deployment.request_fragmentation_session_status if self.fuota_deployment.request_fragmentation_session_status
@ -590,6 +591,20 @@ impl Flow {
.await?; .await?;
} }
let fuota_devices = fuota::get_devices(self.job.fuota_deployment_id.into(), -1, 0).await?;
let fuota_devices: Vec<fuota::FuotaDeploymentDevice> = fuota_devices
.into_iter()
.filter(|d| d.completed_at.is_some() && d.error_msg.is_empty())
.collect();
for fuota_device in &fuota_devices {
let mut d = device::get(&fuota_device.dev_eui).await?;
for (k, v) in self.fuota_deployment.on_complete_set_device_tags.iter() {
d.tags.deref_mut().insert(k.to_string(), v.to_string());
}
let _ = device::update(d).await?;
}
let mut d = self.fuota_deployment.clone(); let mut d = self.fuota_deployment.clone();
d.completed_at = Some(Utc::now()); d.completed_at = Some(Utc::now());
let _ = fuota::update_deployment(d).await?; let _ = fuota::update_deployment(d).await?;

View File

@ -1,3 +1,5 @@
use std::collections::HashMap;
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use chrono::{DateTime, Duration, Utc}; use chrono::{DateTime, Duration, Utc};
use diesel::{dsl, prelude::*}; use diesel::{dsl, prelude::*};
@ -43,6 +45,7 @@ pub struct FuotaDeployment {
pub fragmentation_descriptor: Vec<u8>, pub fragmentation_descriptor: Vec<u8>,
pub request_fragmentation_session_status: fields::RequestFragmentationSessionStatus, pub request_fragmentation_session_status: fields::RequestFragmentationSessionStatus,
pub payload: Vec<u8>, pub payload: Vec<u8>,
pub on_complete_set_device_tags: fields::KeyValue,
} }
impl Default for FuotaDeployment { impl Default for FuotaDeployment {
@ -76,6 +79,7 @@ impl Default for FuotaDeployment {
request_fragmentation_session_status: request_fragmentation_session_status:
fields::RequestFragmentationSessionStatus::NoRequest, fields::RequestFragmentationSessionStatus::NoRequest,
payload: Vec::new(), payload: Vec::new(),
on_complete_set_device_tags: fields::KeyValue::new(HashMap::new()),
} }
} }
} }
@ -227,6 +231,7 @@ pub async fn update_deployment(d: FuotaDeployment) -> Result<FuotaDeployment, Er
fuota_deployment::request_fragmentation_session_status fuota_deployment::request_fragmentation_session_status
.eq(&d.request_fragmentation_session_status), .eq(&d.request_fragmentation_session_status),
fuota_deployment::payload.eq(&d.payload), fuota_deployment::payload.eq(&d.payload),
fuota_deployment::on_complete_set_device_tags.eq(&d.on_complete_set_device_tags),
)) ))
.get_result(&mut get_async_db_conn().await?) .get_result(&mut get_async_db_conn().await?)
.await .await

View File

@ -213,6 +213,7 @@ diesel::table! {
#[max_length = 20] #[max_length = 20]
request_fragmentation_session_status -> Varchar, request_fragmentation_session_status -> Varchar,
payload -> Bytea, payload -> Bytea,
on_complete_set_device_tags -> Jsonb,
} }
} }

View File

@ -189,6 +189,7 @@ diesel::table! {
fragmentation_descriptor -> Binary, fragmentation_descriptor -> Binary,
request_fragmentation_session_status -> Text, request_fragmentation_session_status -> Text,
payload -> Binary, payload -> Binary,
on_complete_set_device_tags -> Text,
} }
} }

View File

@ -1,7 +1,7 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Form, Input, InputNumber, Select, Row, Col, Button, Upload, UploadFile, Switch } from "antd"; import { Tabs, Form, Input, InputNumber, Select, Row, Col, Button, Upload, UploadFile, Switch } from "antd";
import { UploadOutlined } from "@ant-design/icons"; import { UploadOutlined, MinusCircleOutlined, PlusOutlined } from "@ant-design/icons";
import type { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb"; import type { Tenant } from "@chirpstack/chirpstack-api-grpc-web/api/tenant_pb";
import { FuotaDeployment, RequestFragmentationSessionStatus } from "@chirpstack/chirpstack-api-grpc-web/api/fuota_pb"; import { FuotaDeployment, RequestFragmentationSessionStatus } from "@chirpstack/chirpstack-api-grpc-web/api/fuota_pb";
@ -79,6 +79,11 @@ function FuotaDeploymentForm(props: IProps) {
d.setFragmentationFragmentSize(v.fragmentationFragmentSize); d.setFragmentationFragmentSize(v.fragmentationFragmentSize);
d.setPayload(v.payload); d.setPayload(v.payload);
// on complete set device tags
for (const elm of v.onCompleteSetDeviceTagsMap) {
d.getOnCompleteSetDeviceTagsMap().set(elm[0], elm[1]);
}
props.onFinish(d); props.onFinish(d);
}; };
@ -152,206 +157,250 @@ function FuotaDeploymentForm(props: IProps) {
onFinishFailed={onFinishFailed} onFinishFailed={onFinishFailed}
form={form} form={form}
> >
<Form.Item label="Name" name="name" rules={[{ required: true, message: "Please enter a name!" }]}> <Tabs>
<Input disabled={props.disabled} /> <Tabs.TabPane tab="Deployment" key="1">
</Form.Item> <Form.Item label="Name" name="name" rules={[{ required: true, message: "Please enter a name!" }]}>
<Row gutter={24}> <Input disabled={props.disabled} />
<Col span={16}>
<AutocompleteInput
label="Device profile"
name="deviceProfileId"
getOption={getDeviceProfileOption}
getOptions={getDeviceProfileOptions}
disabled={props.disabled || props.update}
required
/>
</Col>
<Col span={8}>
<Form.Item
label="Unicast retry count (max)"
name="unicastMaxRetryCount"
tooltip="This defines how many times ChirpStack will retry unicast commands in case not acknowledged by the end-device."
required
>
<InputNumber min={0} max={5} disabled={props.disabled} style={{ width: "100%" }} />
</Form.Item> </Form.Item>
</Col> <Row gutter={24}>
</Row> <Col span={16}>
<Row gutter={24}> <AutocompleteInput
<Col span={8}> label="Device profile"
<Form.Item name="deviceProfileId"
label="Multicast group-type" getOption={getDeviceProfileOption}
name="multicastGroupType" getOptions={getDeviceProfileOptions}
tooltip="The multicast-group type defines the way how multicast frames are scheduled by the network-server." disabled={props.disabled || props.update}
rules={[{ required: true, message: "Please select a multicast group-type!" }]} required
> />
<Select onChange={onMulticastGroupTypeChange} disabled={props.disabled}> </Col>
<Select.Option value={MulticastGroupType.CLASS_C}>Class-C</Select.Option> <Col span={8}>
<Select.Option value={MulticastGroupType.CLASS_B}>Class-B</Select.Option> <Form.Item
</Select> label="Unicast retry count (max)"
name="unicastMaxRetryCount"
tooltip="This defines how many times ChirpStack will retry unicast commands in case not acknowledged by the end-device."
required
>
<InputNumber min={0} max={5} disabled={props.disabled} style={{ width: "100%" }} />
</Form.Item>
</Col>
</Row>
<Row gutter={24}>
<Col span={8}>
<Form.Item
label="Multicast group-type"
name="multicastGroupType"
tooltip="The multicast-group type defines the way how multicast frames are scheduled by the network-server."
rules={[{ required: true, message: "Please select a multicast group-type!" }]}
>
<Select onChange={onMulticastGroupTypeChange} disabled={props.disabled}>
<Select.Option value={MulticastGroupType.CLASS_C}>Class-C</Select.Option>
<Select.Option value={MulticastGroupType.CLASS_B}>Class-B</Select.Option>
</Select>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label="Class-B ping-slot periodicity" name="multicastClassBPingSlotNbK">
<Select disabled={!isMulticastClassB || props.disabled}>
<Select.Option value={0}>Every second</Select.Option>
<Select.Option value={1}>Every 2 seconds</Select.Option>
<Select.Option value={2}>Every 4 seconds</Select.Option>
<Select.Option value={3}>Every 8 seconds</Select.Option>
<Select.Option value={4}>Every 16 seconds</Select.Option>
<Select.Option value={5}>Every 32 seconds</Select.Option>
<Select.Option value={6}>Every 64 seconds</Select.Option>
<Select.Option value={7}>Every 128 seconds</Select.Option>
</Select>
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
label="Class-C scheduling type"
name="multicastClassCSchedulingType"
tooltip="In order to reach all devices, it might be needed to transmit a downlink through multiple gateways. In case of Delay each gateway will transmit one by one, in case of GPS Time all required gateways will transmit at the same GPS time."
>
<Select disabled={isMulticastClassB || props.disabled}>
<Select.Option value={MulticastGroupSchedulingType.DELAY}>Delay</Select.Option>
<Select.Option value={MulticastGroupSchedulingType.GPS_TIME}>GPS Time</Select.Option>
</Select>
</Form.Item>
</Col>
</Row>
<Row gutter={24}>
<Col span={8}>
<Form.Item
label="Multicast data-rate"
name="multicastDr"
rules={[{ required: true, message: "Please enter a multicast data-rate!" }]}
tooltip="The data-rate to use when transmitting the multicast frames. Please refer to the LoRaWAN Regional Parameters specification for valid values."
>
<InputNumber min={0} max={15} disabled={props.disabled} style={{ width: "100%" }} addonBefore="DR" />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item
label="Multicast frequency (Hz)"
name="multicastFrequency"
tooltip="The frequency to use when transmitting the multicast frames. Please refer to the LoRaWAN Regional Parameters specification for valid values."
rules={[{ required: true, message: "Please enter a frequency!" }]}
>
<InputNumber min={0} disabled={props.disabled} style={{ width: "100%" }} addonAfter="Hz" />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label="Fragmentation redundancy (%)" name="fragmentationRedundancyPercentage">
<InputNumber min={0} max={100} addonAfter="%" style={{ width: "100%" }} disabled={props.disabled} />
</Form.Item>
</Col>
</Row>
<Row gutter={24}>
<Col span={8}>
<Form.Item
label="Fragmentation status request"
name="requestFragmentationSessionStatus"
tooltip="After fragment enqueue is recommended for Class-A devices, after session timeout is recommended for Class-B / Class-C devices."
>
<Select disabled={props.disabled}>
<Select.Option value={RequestFragmentationSessionStatus.NO_REQUEST}>Do not request</Select.Option>
<Select.Option value={RequestFragmentationSessionStatus.AFTER_FRAGMENT_ENQUEUE}>
After fragment enqueue
</Select.Option>
<Select.Option value={RequestFragmentationSessionStatus.AFTER_SESSION_TIMEOUT}>
After session timeout
</Select.Option>
</Select>
</Form.Item>
</Col>
</Row>
<Row gutter={24}>
<Col span={6}>
<Form.Item
label="Calculate multicast-timeout"
name="calculateMulticastTimeout"
tooltip="If checked, ChirpStack will calculate the multicast-timeout."
>
<Switch onChange={(v: boolean) => setCalculateMulticastTimeout(v)} disabled={props.disabled} />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item label="Multicast timeout" name="multicastTimeout">
{isMulticastClassB && (
<Select disabled={props.disabled || calculateMulticastTimeout}>
<Select.Option value={0}>1 beacon period</Select.Option>
<Select.Option value={1}>2 beacon periods</Select.Option>
<Select.Option value={2}>4 beacon periods</Select.Option>
<Select.Option value={3}>8 beacon periods</Select.Option>
<Select.Option value={4}>16 beacon periods</Select.Option>
<Select.Option value={5}>32 beacon periods</Select.Option>
<Select.Option value={6}>64 beacon periods</Select.Option>
<Select.Option value={7}>128 beacon periods</Select.Option>
<Select.Option value={8}>256 beacon periods</Select.Option>
<Select.Option value={9}>512 beacon periods</Select.Option>
<Select.Option value={10}>1024 beacon periods</Select.Option>
<Select.Option value={11}>2048 beacon periods</Select.Option>
<Select.Option value={12}>4096 beacon periods</Select.Option>
<Select.Option value={13}>8192 beacon periods</Select.Option>
<Select.Option value={14}>16384 beacon periods</Select.Option>
<Select.Option value={15}>32768 beacon periods</Select.Option>
</Select>
)}
{!isMulticastClassB && (
<Select disabled={props.disabled || calculateMulticastTimeout}>
<Select.Option value={0}>1 second</Select.Option>
<Select.Option value={1}>2 seconds</Select.Option>
<Select.Option value={2}>4 seconds</Select.Option>
<Select.Option value={3}>8 seconds</Select.Option>
<Select.Option value={4}>16 seconds</Select.Option>
<Select.Option value={5}>32 seconds</Select.Option>
<Select.Option value={6}>64 seconds</Select.Option>
<Select.Option value={7}>128 seconds</Select.Option>
<Select.Option value={8}>256 seconds</Select.Option>
<Select.Option value={9}>512 seconds</Select.Option>
<Select.Option value={10}>1024 seconds</Select.Option>
<Select.Option value={11}>2048 seconds</Select.Option>
<Select.Option value={12}>4096 seconds</Select.Option>
<Select.Option value={13}>8192 seconds</Select.Option>
<Select.Option value={14}>16384 seconds</Select.Option>
<Select.Option value={15}>32768 seconds</Select.Option>
</Select>
)}
</Form.Item>
</Col>
<Col span={6}>
<Form.Item
label="Calculate fragment size"
name="calculateFragmentationFragmentSize"
tooltip="If checked, ChirpStack will calculate the fragment size for fragmentation."
>
<Switch onChange={(v: boolean) => setCalculateFragmentationFragmentSize(v)} disabled={props.disabled} />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item label="Fragment size" name="fragmentationFragmentSize">
<InputNumber
min={0}
max={255}
disabled={props.disabled || calculateFragmentationFragmentSize}
style={{ width: "100%" }}
addonAfter="Bytes"
/>
</Form.Item>
</Col>
</Row>
<Form.Item label="Payload" name="payload" required>
<Upload
beforeUpload={beforeUpload}
onRemove={onRemoveUpload}
maxCount={1}
fileList={fileList}
disabled={props.disabled}
>
<Button icon={<UploadOutlined />} disabled={props.disabled}>
Click to upload
</Button>
</Upload>
</Form.Item> </Form.Item>
</Col> </Tabs.TabPane>
<Col span={8}> <Tabs.TabPane tab="Set device tags (on complete)" key="2">
<Form.Item label="Class-B ping-slot periodicity" name="multicastClassBPingSlotNbK"> <Form.List name="onCompleteSetDeviceTagsMap">
<Select disabled={!isMulticastClassB || props.disabled}> {(fields, { add, remove }) => (
<Select.Option value={0}>Every second</Select.Option> <>
<Select.Option value={1}>Every 2 seconds</Select.Option> {fields.map(({ key, name, ...restField }) => (
<Select.Option value={2}>Every 4 seconds</Select.Option> <Row gutter={24}>
<Select.Option value={3}>Every 8 seconds</Select.Option> <Col span={6}>
<Select.Option value={4}>Every 16 seconds</Select.Option> <Form.Item
<Select.Option value={5}>Every 32 seconds</Select.Option> {...restField}
<Select.Option value={6}>Every 64 seconds</Select.Option> name={[name, 0]}
<Select.Option value={7}>Every 128 seconds</Select.Option> fieldKey={[name, 0]}
</Select> rules={[{ required: true, message: "Please enter a key!" }]}
</Form.Item> >
</Col> <Input placeholder="Key" />
<Col span={8}> </Form.Item>
<Form.Item </Col>
label="Class-C scheduling type" <Col span={16}>
name="multicastClassCSchedulingType" <Form.Item
tooltip="In order to reach all devices, it might be needed to transmit a downlink through multiple gateways. In case of Delay each gateway will transmit one by one, in case of GPS Time all required gateways will transmit at the same GPS time." {...restField}
> name={[name, 1]}
<Select disabled={isMulticastClassB || props.disabled}> fieldKey={[name, 1]}
<Select.Option value={MulticastGroupSchedulingType.DELAY}>Delay</Select.Option> rules={[{ required: true, message: "Please enter a value!" }]}
<Select.Option value={MulticastGroupSchedulingType.GPS_TIME}>GPS Time</Select.Option> >
</Select> <Input placeholder="Value" />
</Form.Item> </Form.Item>
</Col> </Col>
</Row> <Col span={2}>
<Row gutter={24}> <MinusCircleOutlined onClick={() => remove(name)} />
<Col span={8}> </Col>
<Form.Item </Row>
label="Multicast data-rate" ))}
name="multicastDr" <Form.Item>
rules={[{ required: true, message: "Please enter a multicast data-rate!" }]} <Button type="dashed" onClick={() => add()} block icon={<PlusOutlined />}>
tooltip="The data-rate to use when transmitting the multicast frames. Please refer to the LoRaWAN Regional Parameters specification for valid values." Add tag
> </Button>
<InputNumber min={0} max={15} disabled={props.disabled} style={{ width: "100%" }} addonBefore="DR" /> </Form.Item>
</Form.Item> </>
</Col>
<Col span={8}>
<Form.Item
label="Multicast frequency (Hz)"
name="multicastFrequency"
tooltip="The frequency to use when transmitting the multicast frames. Please refer to the LoRaWAN Regional Parameters specification for valid values."
rules={[{ required: true, message: "Please enter a frequency!" }]}
>
<InputNumber min={0} disabled={props.disabled} style={{ width: "100%" }} addonAfter="Hz" />
</Form.Item>
</Col>
<Col span={8}>
<Form.Item label="Fragmentation redundancy (%)" name="fragmentationRedundancyPercentage">
<InputNumber min={0} max={100} addonAfter="%" style={{ width: "100%" }} disabled={props.disabled} />
</Form.Item>
</Col>
</Row>
<Row gutter={24}>
<Col span={8}>
<Form.Item
label="Fragmentation status request"
name="requestFragmentationSessionStatus"
tooltip="After fragment enqueue is recommended for Class-A devices, after session timeout is recommended for Class-B / Class-C devices."
>
<Select disabled={props.disabled}>
<Select.Option value={RequestFragmentationSessionStatus.NO_REQUEST}>Do not request</Select.Option>
<Select.Option value={RequestFragmentationSessionStatus.AFTER_FRAGMENT_ENQUEUE}>
After fragment enqueue
</Select.Option>
<Select.Option value={RequestFragmentationSessionStatus.AFTER_SESSION_TIMEOUT}>
After session timeout
</Select.Option>
</Select>
</Form.Item>
</Col>
</Row>
<Row gutter={24}>
<Col span={6}>
<Form.Item
label="Calculate multicast-timeout"
name="calculateMulticastTimeout"
tooltip="If checked, ChirpStack will calculate the multicast-timeout."
>
<Switch onChange={(v: boolean) => setCalculateMulticastTimeout(v)} disabled={props.disabled} />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item label="Multicast timeout" name="multicastTimeout">
{isMulticastClassB && (
<Select disabled={props.disabled || calculateMulticastTimeout}>
<Select.Option value={0}>1 beacon period</Select.Option>
<Select.Option value={1}>2 beacon periods</Select.Option>
<Select.Option value={2}>4 beacon periods</Select.Option>
<Select.Option value={3}>8 beacon periods</Select.Option>
<Select.Option value={4}>16 beacon periods</Select.Option>
<Select.Option value={5}>32 beacon periods</Select.Option>
<Select.Option value={6}>64 beacon periods</Select.Option>
<Select.Option value={7}>128 beacon periods</Select.Option>
<Select.Option value={8}>256 beacon periods</Select.Option>
<Select.Option value={9}>512 beacon periods</Select.Option>
<Select.Option value={10}>1024 beacon periods</Select.Option>
<Select.Option value={11}>2048 beacon periods</Select.Option>
<Select.Option value={12}>4096 beacon periods</Select.Option>
<Select.Option value={13}>8192 beacon periods</Select.Option>
<Select.Option value={14}>16384 beacon periods</Select.Option>
<Select.Option value={15}>32768 beacon periods</Select.Option>
</Select>
)} )}
{!isMulticastClassB && ( </Form.List>
<Select disabled={props.disabled || calculateMulticastTimeout}> </Tabs.TabPane>
<Select.Option value={0}>1 second</Select.Option> </Tabs>
<Select.Option value={1}>2 seconds</Select.Option>
<Select.Option value={2}>4 seconds</Select.Option>
<Select.Option value={3}>8 seconds</Select.Option>
<Select.Option value={4}>16 seconds</Select.Option>
<Select.Option value={5}>32 seconds</Select.Option>
<Select.Option value={6}>64 seconds</Select.Option>
<Select.Option value={7}>128 seconds</Select.Option>
<Select.Option value={8}>256 seconds</Select.Option>
<Select.Option value={9}>512 seconds</Select.Option>
<Select.Option value={10}>1024 seconds</Select.Option>
<Select.Option value={11}>2048 seconds</Select.Option>
<Select.Option value={12}>4096 seconds</Select.Option>
<Select.Option value={13}>8192 seconds</Select.Option>
<Select.Option value={14}>16384 seconds</Select.Option>
<Select.Option value={15}>32768 seconds</Select.Option>
</Select>
)}
</Form.Item>
</Col>
<Col span={6}>
<Form.Item
label="Calculate fragment size"
name="calculateFragmentationFragmentSize"
tooltip="If checked, ChirpStack will calculate the fragment size for fragmentation."
>
<Switch onChange={(v: boolean) => setCalculateFragmentationFragmentSize(v)} disabled={props.disabled} />
</Form.Item>
</Col>
<Col span={6}>
<Form.Item label="Fragment size" name="fragmentationFragmentSize">
<InputNumber
min={0}
max={255}
disabled={props.disabled || calculateFragmentationFragmentSize}
style={{ width: "100%" }}
addonAfter="Bytes"
/>
</Form.Item>
</Col>
</Row>
<Form.Item label="Payload" name="payload" required>
<Upload
beforeUpload={beforeUpload}
onRemove={onRemoveUpload}
maxCount={1}
fileList={fileList}
disabled={props.disabled}
>
<Button icon={<UploadOutlined />} disabled={props.disabled}>
Click to upload
</Button>
</Upload>
</Form.Item>
<Form.Item> <Form.Item>
<Button type="primary" htmlType="submit" disabled={props.disabled}> <Button type="primary" htmlType="submit" disabled={props.disabled}>
Submit Submit