mirror of
https://github.com/gen2brain/cam2ip.git
synced 2026-07-23 13:25:07 +00:00
Update Android backend
This commit is contained in:
@@ -4,218 +4,223 @@
|
||||
package camera
|
||||
|
||||
/*
|
||||
#include <stdlib.h>
|
||||
|
||||
#include <android/log.h>
|
||||
|
||||
#include <media/NdkImageReader.h>
|
||||
|
||||
#include <camera/NdkCameraDevice.h>
|
||||
#include <camera/NdkCameraManager.h>
|
||||
#include <camera/NdkCameraMetadata.h>
|
||||
|
||||
#define TAG "camera"
|
||||
#define TAG "cam2ip"
|
||||
#define LOGE(...) __android_log_print(ANDROID_LOG_ERROR, TAG, __VA_ARGS__)
|
||||
#define LOGW(...) __android_log_print(ANDROID_LOG_WARN, TAG, __VA_ARGS__)
|
||||
#define LOGI(...) __android_log_print(ANDROID_LOG_INFO, TAG, __VA_ARGS__)
|
||||
#define LOGD(...) __android_log_print(ANDROID_LOG_DEBUG, TAG, __VA_ARGS__)
|
||||
|
||||
AImage *image;
|
||||
AImageReader *imageReader;
|
||||
static AImageReader *imageReader;
|
||||
static ANativeWindow *nativeWindow;
|
||||
|
||||
ANativeWindow *nativeWindow;
|
||||
static ACameraDevice *cameraDevice;
|
||||
static ACameraManager *cameraManager;
|
||||
static ACameraOutputTarget *cameraOutputTarget;
|
||||
static ACameraCaptureSession *cameraCaptureSession;
|
||||
|
||||
ACameraDevice *cameraDevice;
|
||||
ACameraManager *cameraManager;
|
||||
ACameraOutputTarget *cameraOutputTarget;
|
||||
ACameraCaptureSession *cameraCaptureSession;
|
||||
static ACaptureRequest *captureRequest;
|
||||
static ACaptureSessionOutput *captureSessionOutput;
|
||||
static ACaptureSessionOutputContainer *captureSessionOutputContainer;
|
||||
|
||||
ACaptureRequest *captureRequest;
|
||||
ACaptureSessionOutput *captureSessionOutput;
|
||||
ACaptureSessionOutputContainer *captureSessionOutputContainer;
|
||||
|
||||
void device_on_disconnected(void *context, ACameraDevice *device) {
|
||||
LOGI("camera %s is disconnected.\n", ACameraDevice_getId(device));
|
||||
static void device_on_disconnected(void *context, ACameraDevice *device) {
|
||||
LOGI("camera %s disconnected", ACameraDevice_getId(device));
|
||||
}
|
||||
|
||||
void device_on_error(void *context, ACameraDevice *device, int error) {
|
||||
LOGE("error %d on camera %s.\n", error, ACameraDevice_getId(device));
|
||||
static void device_on_error(void *context, ACameraDevice *device, int error) {
|
||||
LOGE("camera %s error %d", ACameraDevice_getId(device), error);
|
||||
}
|
||||
|
||||
ACameraDevice_stateCallbacks deviceStateCallbacks = {
|
||||
.context = NULL,
|
||||
.onDisconnected = device_on_disconnected,
|
||||
.onError = device_on_error,
|
||||
static ACameraDevice_stateCallbacks deviceStateCallbacks = {
|
||||
.onDisconnected = device_on_disconnected,
|
||||
.onError = device_on_error,
|
||||
};
|
||||
|
||||
void session_on_ready(void *context, ACameraCaptureSession *session) {
|
||||
LOGI("session is ready. %p\n", session);
|
||||
}
|
||||
|
||||
void session_on_active(void *context, ACameraCaptureSession *session) {
|
||||
LOGI("session is activated. %p\n", session);
|
||||
}
|
||||
|
||||
void session_on_closed(void *context, ACameraCaptureSession *session) {
|
||||
LOGI("session is closed. %p\n", session);
|
||||
}
|
||||
|
||||
ACameraCaptureSession_stateCallbacks captureSessionStateCallbacks = {
|
||||
.context = NULL,
|
||||
.onActive = session_on_active,
|
||||
.onReady = session_on_ready,
|
||||
.onClosed = session_on_closed,
|
||||
};
|
||||
|
||||
void image_callback(void *context, AImageReader *reader) {
|
||||
LOGD("image_callback");
|
||||
|
||||
media_status_t status = AImageReader_acquireLatestImage(reader, &image);
|
||||
if(status != AMEDIA_OK) {
|
||||
LOGE("failed to acquire next image (reason: %d).\n", status);
|
||||
}
|
||||
}
|
||||
|
||||
AImageReader_ImageListener imageListener = {
|
||||
.context = NULL,
|
||||
.onImageAvailable = image_callback,
|
||||
};
|
||||
static ACameraCaptureSession_stateCallbacks captureSessionStateCallbacks = {0};
|
||||
|
||||
int openCamera(int index, int width, int height) {
|
||||
ACameraIdList *cameraIdList;
|
||||
const char *selectedCameraId;
|
||||
|
||||
camera_status_t status = ACAMERA_OK;
|
||||
ACameraIdList *cameraIdList = NULL;
|
||||
|
||||
cameraManager = ACameraManager_create();
|
||||
|
||||
status = ACameraManager_getCameraIdList(cameraManager, &cameraIdList);
|
||||
if(status != ACAMERA_OK) {
|
||||
LOGE("failed to get camera id list (reason: %d).\n", status);
|
||||
return status;
|
||||
camera_status_t status = ACameraManager_getCameraIdList(cameraManager, &cameraIdList);
|
||||
if (status != ACAMERA_OK) {
|
||||
return status;
|
||||
}
|
||||
|
||||
if(cameraIdList->numCameras < 1) {
|
||||
LOGE("no camera device detected.\n");
|
||||
if (index < 0 || index >= cameraIdList->numCameras) {
|
||||
ACameraManager_deleteCameraIdList(cameraIdList);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if(cameraIdList->numCameras < index+1) {
|
||||
LOGE("no camera at index %d.\n", index);
|
||||
const char *cameraId = cameraIdList->cameraIds[index];
|
||||
LOGI("open camera %s (%d available)", cameraId, cameraIdList->numCameras);
|
||||
|
||||
status = ACameraManager_openCamera(cameraManager, cameraId, &deviceStateCallbacks, &cameraDevice);
|
||||
ACameraManager_deleteCameraIdList(cameraIdList);
|
||||
if (status != ACAMERA_OK) {
|
||||
return status;
|
||||
}
|
||||
|
||||
selectedCameraId = cameraIdList->cameraIds[index];
|
||||
LOGI("open camera (id: %s, num of cameras: %d).\n", selectedCameraId, cameraIdList->numCameras);
|
||||
|
||||
status = ACameraManager_openCamera(cameraManager, selectedCameraId, &deviceStateCallbacks, &cameraDevice);
|
||||
if(status != ACAMERA_OK) {
|
||||
LOGE("failed to open camera device (id: %s)\n", selectedCameraId);
|
||||
return status;
|
||||
media_status_t mstatus = AImageReader_new(width, height, AIMAGE_FORMAT_YUV_420_888, 4, &imageReader);
|
||||
if (mstatus != AMEDIA_OK) {
|
||||
return mstatus;
|
||||
}
|
||||
|
||||
status = ACameraDevice_createCaptureRequest(cameraDevice, TEMPLATE_STILL_CAPTURE, &captureRequest);
|
||||
if(status != ACAMERA_OK) {
|
||||
LOGE("failed to create snapshot capture request (id: %s)\n", selectedCameraId);
|
||||
return status;
|
||||
}
|
||||
|
||||
status = ACaptureSessionOutputContainer_create(&captureSessionOutputContainer);
|
||||
if(status != ACAMERA_OK) {
|
||||
LOGE("failed to create session output container (id: %s)\n", selectedCameraId);
|
||||
return status;
|
||||
}
|
||||
|
||||
media_status_t mstatus = AImageReader_new(width, height, AIMAGE_FORMAT_YUV_420_888, 2, &imageReader);
|
||||
if(mstatus != AMEDIA_OK) {
|
||||
LOGE("failed to create image reader (reason: %d).\n", mstatus);
|
||||
return mstatus;
|
||||
}
|
||||
|
||||
mstatus = AImageReader_setImageListener(imageReader, &imageListener);
|
||||
if(mstatus != AMEDIA_OK) {
|
||||
LOGE("failed to set image listener (reason: %d).\n", mstatus);
|
||||
return mstatus;
|
||||
}
|
||||
|
||||
AImageReader_getWindow(imageReader, &nativeWindow);
|
||||
AImageReader_getWindow(imageReader, &nativeWindow);
|
||||
ANativeWindow_acquire(nativeWindow);
|
||||
|
||||
status = ACameraDevice_createCaptureRequest(cameraDevice, TEMPLATE_PREVIEW, &captureRequest);
|
||||
if (status != ACAMERA_OK) {
|
||||
return status;
|
||||
}
|
||||
|
||||
// Auto exposure and a frame rate range so the sensor streams continuously.
|
||||
uint8_t controlMode = ACAMERA_CONTROL_MODE_AUTO;
|
||||
ACaptureRequest_setEntry_u8(captureRequest, ACAMERA_CONTROL_MODE, 1, &controlMode);
|
||||
uint8_t aeMode = ACAMERA_CONTROL_AE_MODE_ON;
|
||||
ACaptureRequest_setEntry_u8(captureRequest, ACAMERA_CONTROL_AE_MODE, 1, &aeMode);
|
||||
int32_t fpsRange[2] = {15, 30};
|
||||
ACaptureRequest_setEntry_i32(captureRequest, ACAMERA_CONTROL_AE_TARGET_FPS_RANGE, 2, fpsRange);
|
||||
|
||||
ACameraOutputTarget_create(nativeWindow, &cameraOutputTarget);
|
||||
ACaptureRequest_addTarget(captureRequest, cameraOutputTarget);
|
||||
|
||||
ACaptureSessionOutputContainer_create(&captureSessionOutputContainer);
|
||||
ACaptureSessionOutput_create(nativeWindow, &captureSessionOutput);
|
||||
ACaptureSessionOutputContainer_add(captureSessionOutputContainer, captureSessionOutput);
|
||||
ACaptureSessionOutputContainer_add(captureSessionOutputContainer, captureSessionOutput);
|
||||
|
||||
status = ACameraDevice_createCaptureSession(cameraDevice, captureSessionOutputContainer, &captureSessionStateCallbacks, &cameraCaptureSession);
|
||||
if(status != ACAMERA_OK) {
|
||||
LOGE("failed to create capture session (reason: %d).\n", status);
|
||||
return status;
|
||||
if (status != ACAMERA_OK) {
|
||||
return status;
|
||||
}
|
||||
|
||||
ACameraManager_deleteCameraIdList(cameraIdList);
|
||||
ACameraManager_delete(cameraManager);
|
||||
status = ACameraCaptureSession_setRepeatingRequest(cameraCaptureSession, NULL, 1, &captureRequest, NULL);
|
||||
if (status != ACAMERA_OK) {
|
||||
return status;
|
||||
}
|
||||
|
||||
return ACAMERA_OK;
|
||||
}
|
||||
|
||||
int captureCamera() {
|
||||
camera_status_t status = ACameraCaptureSession_capture(cameraCaptureSession, NULL, 1, &captureRequest, NULL);
|
||||
if(status != ACAMERA_OK) {
|
||||
LOGE("failed to capture image (reason: %d).\n", status);
|
||||
// acquireImage releases the previous frame and acquires the most recent one.
|
||||
int acquireImage() {
|
||||
if (image != NULL) {
|
||||
AImage_delete(image);
|
||||
image = NULL;
|
||||
}
|
||||
|
||||
return status;
|
||||
return AImageReader_acquireLatestImage(imageReader, &image);
|
||||
}
|
||||
|
||||
int closeCamera() {
|
||||
camera_status_t status = ACAMERA_OK;
|
||||
if (cameraCaptureSession != NULL) {
|
||||
ACameraCaptureSession_stopRepeating(cameraCaptureSession);
|
||||
ACameraCaptureSession_close(cameraCaptureSession);
|
||||
cameraCaptureSession = NULL;
|
||||
}
|
||||
|
||||
if(captureRequest != NULL) {
|
||||
if (captureRequest != NULL) {
|
||||
ACaptureRequest_free(captureRequest);
|
||||
captureRequest = NULL;
|
||||
}
|
||||
|
||||
if(cameraOutputTarget != NULL) {
|
||||
if (cameraOutputTarget != NULL) {
|
||||
ACameraOutputTarget_free(cameraOutputTarget);
|
||||
cameraOutputTarget = NULL;
|
||||
}
|
||||
|
||||
if(cameraDevice != NULL) {
|
||||
status = ACameraDevice_close(cameraDevice);
|
||||
|
||||
if(status != ACAMERA_OK) {
|
||||
LOGE("failed to close camera device.\n");
|
||||
return status;
|
||||
}
|
||||
|
||||
cameraDevice = NULL;
|
||||
if (cameraDevice != NULL) {
|
||||
ACameraDevice_close(cameraDevice);
|
||||
cameraDevice = NULL;
|
||||
}
|
||||
|
||||
if(captureSessionOutput != NULL) {
|
||||
if (captureSessionOutput != NULL) {
|
||||
ACaptureSessionOutput_free(captureSessionOutput);
|
||||
captureSessionOutput = NULL;
|
||||
}
|
||||
|
||||
if(captureSessionOutputContainer != NULL) {
|
||||
if (captureSessionOutputContainer != NULL) {
|
||||
ACaptureSessionOutputContainer_free(captureSessionOutputContainer);
|
||||
captureSessionOutputContainer = NULL;
|
||||
}
|
||||
|
||||
if(imageReader != NULL) {
|
||||
AImageReader_delete(imageReader);
|
||||
imageReader = NULL;
|
||||
if (nativeWindow != NULL) {
|
||||
ANativeWindow_release(nativeWindow);
|
||||
nativeWindow = NULL;
|
||||
}
|
||||
|
||||
if(image != NULL) {
|
||||
AImage_delete(image);
|
||||
image = NULL;
|
||||
}
|
||||
if (imageReader != NULL) {
|
||||
AImageReader_delete(imageReader);
|
||||
imageReader = NULL;
|
||||
}
|
||||
|
||||
if (image != NULL) {
|
||||
AImage_delete(image);
|
||||
image = NULL;
|
||||
}
|
||||
|
||||
if (cameraManager != NULL) {
|
||||
ACameraManager_delete(cameraManager);
|
||||
cameraManager = NULL;
|
||||
}
|
||||
|
||||
LOGI("camera closed.\n");
|
||||
return ACAMERA_OK;
|
||||
}
|
||||
|
||||
int openCamera(int index, int width, int height);
|
||||
int captureCamera();
|
||||
int closeCamera();
|
||||
static ACameraManager *enumManager;
|
||||
static ACameraIdList *enumList;
|
||||
|
||||
int camerasOpen() {
|
||||
enumManager = ACameraManager_create();
|
||||
if (ACameraManager_getCameraIdList(enumManager, &enumList) != ACAMERA_OK) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
return enumList->numCameras;
|
||||
}
|
||||
|
||||
const char *cameraIdAt(int i) {
|
||||
return enumList->cameraIds[i];
|
||||
}
|
||||
|
||||
int cameraFacingAt(int i) {
|
||||
ACameraMetadata *meta;
|
||||
if (ACameraManager_getCameraCharacteristics(enumManager, enumList->cameraIds[i], &meta) != ACAMERA_OK) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int facing = -1;
|
||||
ACameraMetadata_const_entry entry;
|
||||
if (ACameraMetadata_getConstEntry(meta, ACAMERA_LENS_FACING, &entry) == ACAMERA_OK) {
|
||||
facing = entry.data.u8[0];
|
||||
}
|
||||
|
||||
ACameraMetadata_free(meta);
|
||||
|
||||
return facing;
|
||||
}
|
||||
|
||||
void camerasClose() {
|
||||
if (enumList != NULL) {
|
||||
ACameraManager_deleteCameraIdList(enumList);
|
||||
enumList = NULL;
|
||||
}
|
||||
|
||||
if (enumManager != NULL) {
|
||||
ACameraManager_delete(enumManager);
|
||||
enumManager = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
#cgo android CFLAGS: -D__ANDROID_API__=24
|
||||
#cgo android LDFLAGS: -lcamera2ndk -lmediandk -llog -landroid
|
||||
*/
|
||||
import "C"
|
||||
@@ -223,81 +228,145 @@ import "C"
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"time"
|
||||
"unsafe"
|
||||
|
||||
im "github.com/gen2brain/cam2ip/image"
|
||||
)
|
||||
|
||||
// Camera represents camera.
|
||||
type Camera struct {
|
||||
opts Options
|
||||
img *image.YCbCr
|
||||
opts Options
|
||||
width int
|
||||
height int
|
||||
ycbcr *image.YCbCr
|
||||
}
|
||||
|
||||
// New returns new Camera for given camera index.
|
||||
func New(opts Options) (camera *Camera, err error) {
|
||||
camera = &Camera{}
|
||||
camera.opts = opts
|
||||
func New(opts Options) (*Camera, error) {
|
||||
c := &Camera{opts: opts}
|
||||
c.width = int(opts.Width)
|
||||
c.height = int(opts.Height)
|
||||
c.ycbcr = image.NewYCbCr(image.Rect(0, 0, c.width, c.height), image.YCbCrSubsampleRatio420)
|
||||
|
||||
camera.img = image.NewYCbCr(image.Rect(0, 0, int(opts.Width), int(opts.Height)), image.YCbCrSubsampleRatio420)
|
||||
|
||||
ret := C.openCamera(C.int(opts.Index), C.int(opts.Width), C.int(opts.Height))
|
||||
ret := C.openCamera(C.int(opts.Index), C.int(c.width), C.int(c.height))
|
||||
if int(ret) != 0 {
|
||||
err = fmt.Errorf("camera: can not open camera %d: error %d", opts.Index, int(ret))
|
||||
|
||||
return
|
||||
return nil, fmt.Errorf("camera: can not open camera %d: error %d", opts.Index, int(ret))
|
||||
}
|
||||
|
||||
return
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// Read reads next frame from camera and returns image.
|
||||
func (c *Camera) Read() (img image.Image, err error) {
|
||||
ret := C.captureCamera()
|
||||
if int(ret) != 0 {
|
||||
err = fmt.Errorf("camera: can not grab frame: error %d", int(ret))
|
||||
for i := 0; ; i++ {
|
||||
if int(C.acquireImage()) == 0 && C.image != nil {
|
||||
break
|
||||
}
|
||||
|
||||
return
|
||||
if i == 99 {
|
||||
return nil, fmt.Errorf("camera: can not retrieve frame")
|
||||
}
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
if C.image == nil {
|
||||
err = fmt.Errorf("camera: can not retrieve frame")
|
||||
c.convert()
|
||||
|
||||
return
|
||||
img = c.ycbcr
|
||||
|
||||
if c.opts.Rotate != 0 {
|
||||
img = im.Rotate(img, c.opts.Rotate)
|
||||
}
|
||||
|
||||
var yStride C.int
|
||||
var yLen, cbLen, crLen C.int
|
||||
var yPtr, cbPtr, crPtr *C.uint8_t
|
||||
if c.opts.Flip != "" {
|
||||
img = im.Flip(img, c.opts.Flip)
|
||||
}
|
||||
|
||||
if c.opts.Timestamp {
|
||||
img = im.Timestamp(img, c.opts.TimeFormat)
|
||||
}
|
||||
|
||||
return img, nil
|
||||
}
|
||||
|
||||
// convert copies the YUV_420_888 planes into the YCbCr image, handling row and pixel strides (planar or semi-planar).
|
||||
func (c *Camera) convert() {
|
||||
var yStride, uStride, vStride C.int32_t
|
||||
var uPixel, vPixel C.int32_t
|
||||
var yPtr, uPtr, vPtr *C.uint8_t
|
||||
var yLen, uLen, vLen C.int
|
||||
|
||||
C.AImage_getPlaneRowStride(C.image, 0, &yStride)
|
||||
C.AImage_getPlaneRowStride(C.image, 1, &uStride)
|
||||
C.AImage_getPlaneRowStride(C.image, 2, &vStride)
|
||||
C.AImage_getPlanePixelStride(C.image, 1, &uPixel)
|
||||
C.AImage_getPlanePixelStride(C.image, 2, &vPixel)
|
||||
C.AImage_getPlaneData(C.image, 0, &yPtr, &yLen)
|
||||
C.AImage_getPlaneData(C.image, 1, &cbPtr, &cbLen)
|
||||
C.AImage_getPlaneData(C.image, 2, &crPtr, &crLen)
|
||||
C.AImage_getPlaneData(C.image, 1, &uPtr, &uLen)
|
||||
C.AImage_getPlaneData(C.image, 2, &vPtr, &vLen)
|
||||
|
||||
c.img.YStride = int(yStride)
|
||||
c.img.CStride = int(yStride) / 2
|
||||
ySrc := unsafe.Slice((*byte)(unsafe.Pointer(yPtr)), int(yLen))
|
||||
uSrc := unsafe.Slice((*byte)(unsafe.Pointer(uPtr)), int(uLen))
|
||||
vSrc := unsafe.Slice((*byte)(unsafe.Pointer(vPtr)), int(vLen))
|
||||
|
||||
c.img.Y = C.GoBytes(unsafe.Pointer(yPtr), yLen)
|
||||
c.img.Cb = C.GoBytes(unsafe.Pointer(cbPtr), cbLen)
|
||||
c.img.Cr = C.GoBytes(unsafe.Pointer(crPtr), crLen)
|
||||
ys := int(yStride)
|
||||
for r := 0; r < c.height; r++ {
|
||||
copy(c.ycbcr.Y[r*c.ycbcr.YStride:r*c.ycbcr.YStride+c.width], ySrc[r*ys:r*ys+c.width])
|
||||
}
|
||||
|
||||
img = c.img
|
||||
cw, ch := c.width/2, c.height/2
|
||||
us, up := int(uStride), int(uPixel)
|
||||
vs, vp := int(vStride), int(vPixel)
|
||||
|
||||
return
|
||||
for r := 0; r < ch; r++ {
|
||||
for col := 0; col < cw; col++ {
|
||||
c.ycbcr.Cb[r*c.ycbcr.CStride+col] = uSrc[r*us+col*up]
|
||||
c.ycbcr.Cr[r*c.ycbcr.CStride+col] = vSrc[r*vs+col*vp]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Close closes camera.
|
||||
func (c *Camera) Close() (err error) {
|
||||
ret := C.closeCamera()
|
||||
if int(ret) != 0 {
|
||||
err = fmt.Errorf("camera: can not close camera %d: error %d", c.opts.Index, int(ret))
|
||||
|
||||
return
|
||||
func (c *Camera) Close() error {
|
||||
if int(C.closeCamera()) != 0 {
|
||||
return fmt.Errorf("camera: can not close camera %d", c.opts.Index)
|
||||
}
|
||||
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
// Info returns the negotiated capture format.
|
||||
func (c *Camera) Info() Info {
|
||||
return Info{Format: "YUV420", Width: c.width, Height: c.height}
|
||||
}
|
||||
|
||||
// Devices returns the available capture devices.
|
||||
func Devices() ([]DeviceInfo, error) {
|
||||
return nil, fmt.Errorf("camera: device enumeration not implemented on android")
|
||||
count := int(C.camerasOpen())
|
||||
if count < 0 {
|
||||
C.camerasClose()
|
||||
|
||||
return nil, fmt.Errorf("camera: can not list cameras")
|
||||
}
|
||||
defer C.camerasClose()
|
||||
|
||||
devices := make([]DeviceInfo, 0, count)
|
||||
for i := 0; i < count; i++ {
|
||||
id := C.GoString(C.cameraIdAt(C.int(i)))
|
||||
|
||||
name := id
|
||||
switch int(C.cameraFacingAt(C.int(i))) {
|
||||
case 0:
|
||||
name = id + " (front)"
|
||||
case 1:
|
||||
name = id + " (back)"
|
||||
case 2:
|
||||
name = id + " (external)"
|
||||
}
|
||||
|
||||
devices = append(devices, DeviceInfo{Index: i, Name: name})
|
||||
}
|
||||
|
||||
return devices, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user