پروژه با آردوینو

ساخت رادار با آردوینو و اپلیکیشن فلاتر (Flutter)

آردوینو فلاتر رادار

در این پروژه هیجان‌انگیز، ما دنیای سخت‌افزار و نرم‌افزار را به هم پیوند می‌دهیم تا یک سیستم رادار آردوینو فلاتر کاملاً کاربردی بسازیم. با استفاده از یک برد آردوینو، یک سنسور التراسونیک و یک سروو موتور، محیط اطراف را اسکن می‌کنیم و داده‌های آن را به صورت زنده در یک اپلیکیشن دسکتاپ زیبا که با فلاتر (Flutter) ساخته شده، نمایش می‌دهیم. این پروژه نه تنها یک تمرین عالی برای یادگیری الکترونیک و برنامه‌نویسی است، بلکه نتیجه نهایی آن یک گجت جذاب و دیدنی است.

بخش سخت‌افزار: چشم‌های رادار

قلب سخت‌افزاری این پروژه یک برد آردوینو است که وظیفه کنترل قطعات و جمع‌آوری داده‌ها را بر عهده دارد.

قطعات مورد نیاز:

    • برد آردوینو (مدل Uno یا هر مدل مشابه)
    • سنسور فاصله التراسونیک HC-SR04
    • سروو موتور (مدل SG90 یا مشابه)
    • یک عدد LED یا ماژول لیزر
    • بردبورد و سیم‌های جامپر
    • مقاومت 220 اهم (برای LED) در صورتی که از ماژول لیزر استفاده می کنید لازم نیست

نحوه عملکرد سخت‌افزار:

  1. سروو موتور: این موتور کوچک وظیفه دارد سنسور التراسونیک را مانند یک رادار واقعی، از زاویه 0 تا 180 درجه بچرخاند و سپس به حالت اول بازگردد.

  2. سنسور HC-SR04: این سنسور که چشم‌های رادار ماست، در هر زاویه‌ای که سروو قرار می‌گیرد، با ارسال امواج صوتی و محاسبه زمان بازگشت آن‌ها، فاصله تا نزدیک‌ترین شیء را اندازه‌گیری می‌کند. این سنسور دارای یک زاویه دید تقریبی 30 درجه است که ما آن را در نرم‌افزار شبیه‌سازی کرده‌ایم.

  3. LED/لیزر: به عنوان یک سیستم هشدار عمل می‌کند. هرگاه سنسور، شیئی را در فاصله کمتر از 30 سانتی‌متری تشخیص دهد، این LED روشن می‌شود.

  4. آردوینو: مغز متفکر سیستم است. آردوینو سروو را کنترل می‌کند، داده‌ها را از سنسور می‌خواند، وضعیت لیزر را مدیریت کرده و در نهایت، اطلاعات زاویه و فاصله را از طریق پورت USB (ارتباط سریال) برای اپلیکیشن فلاتر ارسال می‌کند

				
					/*
  رادار آردوینو با سنسور HC-SR04 و سروو موتور
  
  نویسنده: مهدی بهرام
  شرکت: مخترعین شاتوت الکترونیک
  وبسایت: shahtut.com

  این کد یک سروو موتور را مانند رادار به جلو و عقب حرکت می‌دهد. در هر مرحله،
  از یک سنسور التراسونیک HC-SR04 برای اندازه‌گیری فاصله تا نزدیک‌ترین شی استفاده می‌کند.
  سپس داده‌های زاویه و فاصله را از طریق پورت سریال با فرمت "angle,distance" ارسال می‌کند.
  
  اتصالات سخت‌افزاری:
  - سروو موتور:
    - سیم VCC (قرمز)      -> 5V آردوینو
    - سیم GND (قهوه‌ای/سیاه) -> GND آردوینو
    - سیم سیگنال (نارنجی/زرد) -> پین 9 آردوینو
    
  - سنسور التراسونیک HC-SR04:
    - VCC                 -> 5V آردوینو
    - GND                 -> GND آردوینو
    - پایه Trig            -> پین 10 آردوینو
    - پایه Echo            -> پین 11 آردوینو

  - ال‌ای‌دی / لیزر (برای هشدار نزدیکی):
    - پایه مثبت (بلندتر)   -> پین 12 آردوینو
    - پایه منفی (کوتاه‌تر)  -> GND آردوینو (از طریق یک مقاومت 220 اهم)
*/
#include <Arduino.h>
#include <Servo.h>

// تعریف پین‌ها برای سروو، سنسور التراسونیک و ال‌ای‌دی
const int SERVO_PIN = 9;
const int TRIG_PIN = 10;
const int ECHO_PIN = 11;
const int LED_PIN = 12; // پین برای ال‌ای‌دی یا لیزر

// ایجاد یک شیء سروو
Servo radarServo;



/**
 * اندازه‌گیری فاصله با استفاده از سنسور HC-SR04.
 * @return فاصله به سانتی‌متر. اگر خارج از محدوده باشد، 300 را برمی‌گرداند.
 */
int getDistance() {
  // سنسور با یک پالس HIGH به مدت 10 میکروثانیه یا بیشتر فعال می‌شود.
  // ابتدا یک پالس LOW کوتاه می‌دهیم تا از یک پالس HIGH تمیز اطمینان حاصل کنیم.
  digitalWrite(TRIG_PIN, LOW);
  delayMicroseconds(2);
  digitalWrite(TRIG_PIN, HIGH);
  delayMicroseconds(10);
  digitalWrite(TRIG_PIN, LOW);
  
  // تابع pulseIn() یک پالس را روی پایه echo می‌خواند.
  // این تابع طول پالس را به میکروثانیه برمی‌گرداند.
  long duration = pulseIn(ECHO_PIN, HIGH);
  
  // سرعت صوت 343 متر بر ثانیه یا 0.0343 سانتی‌متر بر میکروثانیه است.
  // پالس به سمت شیء رفته و برمی‌گردد، بنابراین نتیجه را بر 2 تقسیم می‌کنیم.
  // فاصله = (مدت زمان * سرعت صوت) / 2
  int distance = duration * 0.0343 / 2;
  
  // محدود کردن فاصله به حداکثر 300 سانتی‌متر برای نمایش بهتر
  if (distance > 300 || distance <= 0) {
    return 300; // اگر شیئی شناسایی نشد، مقدار حداکثر را برگردان
  }
  
  return distance;
}


void setup() {
  // شروع ارتباط سریال با سرعت 9600 بیت بر ثانیه
  Serial.begin(9600);
  
  // اتصال سروو به پین تعریف شده
  radarServo.attach(SERVO_PIN);
  
  // پیکربندی پین‌های سنسور التراسونیک
  pinMode(TRIG_PIN, OUTPUT);
  pinMode(ECHO_PIN, INPUT);

  // پیکربندی پین ال‌ای‌دی به عنوان خروجی
  pinMode(LED_PIN, OUTPUT);
  digitalWrite(LED_PIN, LOW); // اطمینان از خاموش بودن ال‌ای‌دی در ابتدا
}

void loop() {
  // حرکت از چپ به راست (0 تا 180 درجه)
  for (int angle = 0; angle <= 180; angle++) {
    radarServo.write(angle); // حرکت سروو به زاویه مورد نظر
    int distance = getDistance(); // خواندن فاصله
    
    // ارسال داده‌ها از طریق سریال با فرمت "angle,distance"
    // **نکته مهم:** تمام فاصله‌های اندازه‌گیری شده (تا 300 سانتی‌متر) به اپلیکیشن ارسال می‌شوند.
    Serial.print(angle);
    Serial.print(",");
    Serial.println(distance);

    // بررسی فاصله **فقط** برای روشن کردن ال‌ای‌دی.
    // این بخش تاثیری بر داده ارسالی به اپلیکیشن ندارد.
    if (distance < 30) {
      digitalWrite(LED_PIN, HIGH); // اگر فاصله کمتر از 30 سانتی‌متر بود، ال‌ای‌دی را روشن کن
    } else {
      digitalWrite(LED_PIN, LOW); // در غیر این صورت، آن را خاموش کن
    }
    
    delay(50); // تاخیر کوتاه برای پایداری سروو و سنسور
  }
  
  // حرکت از راست به چپ (180 تا 0 درجه)
  for (int angle = 180; angle >= 0; angle--) {
    radarServo.write(angle);
    int distance = getDistance();
    
    Serial.print(angle);
    Serial.print(",");
    Serial.println(distance);

    if (distance < 30) {
      digitalWrite(LED_PIN, HIGH);
    } else {
      digitalWrite(LED_PIN, LOW);
    }
    
    delay(50);
  }
}

				
			

بخش نرم‌افزار: رابط کاربری مدرن با فلاتر

برای نمایش داده‌های رادار، ما یک اپلیکیشن دسکتاپ برای ویندوز با استفاده از فریمورک فلاتر گوگل طراحی کرده‌ایم. این اپلیکیشن با ظاهری مدرن و کلاسیک، داده‌های خام دریافتی از آردوینو را به یک تصویر گرافیکی و قابل فهم تبدیل می‌کند.

ویژگی‌های کلیدی اپلیکیشن:

    • رابط کاربری فارسی و راست-به-چپ (RTL): کل اپلیکیشن برای کاربران فارسی‌زبان بهینه‌سازی شده است.

    • طراحی واکنش‌گرا (Responsive): ظاهر برنامه به طور خودکار با اندازه‌های مختلف پنجره، از دسکتاپ تا صفحه موبایل، تطبیق پیدا خواهد کرد

    • نمایش زنده داده‌ها: اپلیکیشن به پورت سریال متصل شده و داده‌ها را به محض دریافت از آردوینو، روی صفحه نمایش می‌دهد.

    • افکت دنباله خط رادار: خط اسکنر رادار دارای یک دنباله محو شونده است که حس یک صفحه رادار واقعی را القا کند

    • افکت "فسفر سوز" برای اشیاء: اشیاء شناسایی شده به صورت ناگهانی ظاهر نمی‌شوند؛ بلکه با درخشش اولیه ظاهر شده و سپس به آرامی محو می‌شوند که جلوه‌ای بسیار زیبا و کلاسیک ایجاد می‌کند.

    • شبیه‌سازی زاویه دید سنسور: هر شیء شناسایی شده به صورت یک قوس 30 درجه‌ای نمایش داده می‌شود تا زاویه دید واقعی سنسور HC-SR04 را بهتر نشان دهد.

    • سیستم هشدار گرافیکی: علاوه بر روشن شدن لیزر در سخت‌افزار، یک نوار هشدار قرمز رنگ در بالای صفحه اپلیکیشن ظاهر می‌شود تا نزدیکی بیش از حد اشیاء را به کاربر اطلاع بده

				
					import 'dart:async';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:libserialport/libserialport.dart';

/*
  اپلیکیشن نمایشگر رادار آردوینو
  
  نویسنده: مهدی بهرام
  شرکت: مخترعین شاتوت الکترونیک
  وبسایت: shahtut.com
*/

// یک کلاس برای نگهداری اطلاعات هر نقطه شناسایی شده به همراه زمان آن
class RadarPoint {
  final double angle;
  final double distance;
  final int timestamp; // زمان ثبت به میلی‌ثانیه

  RadarPoint({required this.angle, required this.distance, required this.timestamp});
}


void main() {
  runApp(const RadarApp());
}

class RadarApp extends StatelessWidget {
  const RadarApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'رادار آردوینو',
      debugShowCheckedModeBanner: false,
      // --- پیکربندی برای زبان فارسی و چیدمان راست-به-چپ ---
      locale: const Locale('fa', 'IR'),
      supportedLocales: const [
        Locale('fa', 'IR'),
        Locale('en', 'US'),
      ],
      localizationsDelegates: const [
        GlobalMaterialLocalizations.delegate,
        GlobalWidgetsLocalizations.delegate,
        GlobalCupertinoLocalizations.delegate,
      ],
      theme: ThemeData(
        primarySwatch: Colors.teal,
        fontFamily: 'Vazir', // اطمینان حاصل کنید که فونت فارسی مانند 'وزیر' به پروژه اضافه شده باشد
      ),
      home: const Directionality(
        textDirection: TextDirection.rtl,
        child: RadarHomePage(),
      ),
    );
  }
}

class RadarHomePage extends StatefulWidget {
  const RadarHomePage({super.key});

  @override
  State<RadarHomePage> createState() => _RadarHomePageState();
}

class _RadarHomePageState extends State<RadarHomePage> {
  List<String> _availablePorts = [];
  String? _selectedPort;
  SerialPort? _serialPort;
  StreamSubscription<Uint8List>? _subscription;
  bool _isConnected = false;

  final List<RadarPoint> _radarPoints = []; // استفاده از کلاس جدید برای نقاط
  double _currentAngle = 0;
  final int _maxDistance = 300; // سانتی‌متر، باید با کد آردوینو یکسان باشد
  bool _isObjectClose = false; // برای نمایش هشدار نزدیکی شیء

  @override
  void initState() {
    super.initState();
    _getAvailablePorts();
  }

  @override
  void dispose() {
    _disconnect();
    super.dispose();
  }

  void _getAvailablePorts() {
    setState(() {
      _availablePorts = SerialPort.availablePorts;
    });
    if (_availablePorts.isNotEmpty) {
      setState(() {
        _selectedPort = _availablePorts.first;
      });
    }
  }

  Future<void> _connect() async {
    if (_selectedPort == null) return;

    try {
      _serialPort = SerialPort(_selectedPort!);
      if (!_serialPort!.openReadWrite()) {
        throw SerialPortError("باز کردن پورت ${_serialPort?.name} با شکست مواجه شد");
      }
      
      final config = SerialPortConfig()
        ..baudRate = 9600
        ..bits = 8
        ..parity = SerialPortParity.none
        ..stopBits = 1;
      _serialPort!.config = config;
      
      setState(() {
        _isConnected = true;
      });

      _listenToData();
    } catch (e) {
      _showErrorDialog('خطا در اتصال', e.toString());
      if (_serialPort?.isOpen ?? false) {
        _serialPort!.close();
      }
      setState(() {
        _isConnected = false;
      });
    }
  }

  void _listenToData() {
    final reader = SerialPortReader(_serialPort!);
    StringBuffer buffer = StringBuffer();

    _subscription = reader.stream.listen((data) {
      String text = String.fromCharCodes(data);
      buffer.write(text);

      String bufferString = buffer.toString();
      while (bufferString.contains('\n')) {
        int newlineIndex = bufferString.indexOf('\n');
        String line = bufferString.substring(0, newlineIndex).trim();
        bufferString = bufferString.substring(newlineIndex + 1);

        if (line.isNotEmpty) {
          _parseData(line);
        }
      }
      buffer = StringBuffer(bufferString);
    }, onError: (error) {
      _showErrorDialog('خطای پورت سریال', error.toString());
      _disconnect();
    }, onDone: () {
      _disconnect();
    });
  }
  
  void _parseData(String data) {
      final parts = data.split(',');
      if (parts.length == 2) {
        try {
          final angle = double.tryParse(parts[0]);
          final distance = double.tryParse(parts[1]);

          if (angle != null && distance != null) {
            final now = DateTime.now().millisecondsSinceEpoch;
            setState(() {
              _currentAngle = angle;
              _radarPoints.add(RadarPoint(angle: angle, distance: distance, timestamp: now));
              _isObjectClose = distance < 30; // وضعیت هشدار را بر اساس آخرین فاصله به‌روزرسانی کن
              
              // نقاط قدیمی‌تر از 5 ثانیه را حذف کن تا افکت محو شدن ایجاد شود
              _radarPoints.removeWhere((p) => now - p.timestamp > 5000);
            });
          }
        } catch (e) {
          // خطاهای parse را نادیده بگیر
        }
      }
  }


  void _disconnect() {
    _subscription?.cancel();
    _subscription = null;
    if (_serialPort?.isOpen ?? false) {
      _serialPort!.close();
      _serialPort!.dispose();
    }
    _serialPort = null;
    setState(() {
      _isConnected = false;
      _radarPoints.clear();
      _currentAngle = 0;
      _isObjectClose = false;
    });
  }

  void _showErrorDialog(String title, String content) {
    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: Text(title),
        content: Text(content),
        actions: [
          TextButton(
            child: const Text('باشه'),
            onPressed: () => Navigator.of(context).pop(),
          ),
        ],
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('نمایشگر رادار آردوینو'),
        centerTitle: true,
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          children: [
            _buildControlPanel(),
            const SizedBox(height: 20),
            // ویجت هشدار برای نزدیکی شیء
            if (_isObjectClose)
              Container(
                width: double.infinity,
                padding: const EdgeInsets.all(8.0),
                color: Colors.red.shade700,
                child: const Text(
                  '!!! خطر: شیء در فاصله کمتر از ۳۰ سانتی‌متر',
                  textAlign: TextAlign.center,
                  style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
                ),
              ),
            if (_isObjectClose) const SizedBox(height: 10),
            Expanded(
              child: AspectRatio(
                aspectRatio: 1.7, // حفظ نسبت ابعاد ثابت برای رادار
                child: Container(
                  decoration: BoxDecoration(
                    color: Colors.black,
                    borderRadius: BorderRadius.circular(8),
                    border: Border.all(color: Colors.teal.shade200, width: 2),
                  ),
                  child: ClipRRect(
                    borderRadius: BorderRadius.circular(6),
                    child: CustomPaint(
                      painter: RadarPainter(points: _radarPoints, currentAngle: _currentAngle, maxDistance: _maxDistance),
                      size: Size.infinite,
                    ),
                  ),
                ),
              ),
            ),
            const SizedBox(height: 10),
            Text(
                _isConnected 
                ? 'متصل به: $_selectedPort' 
                : 'قطع شده. لطفا یک پورت را انتخاب و متصل شوید.',
                style: TextStyle(color: _isConnected ? Colors.green : Colors.red),
            ),
            const SizedBox(height: 10),
            // فوتر با لوگو و متن شرکت
            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                Icon(Icons.radar, color: Colors.grey.shade600, size: 24), // جایگاه لوگو
                const SizedBox(width: 10),
                Text(
                  'طراحی شده توسط شرکت مخترعین شاتوت الکترونیک',
                  style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }

  Widget _buildControlPanel() {
    // استفاده از LayoutBuilder برای ساخت پنل کنترل واکنش‌گرا
    return LayoutBuilder(
      builder: (context, constraints) {
        bool isWide = constraints.maxWidth > 450;
        
        List<Widget> children = [
          Expanded(
            child: DropdownButton<String>(
              value: _selectedPort,
              isExpanded: true,
              hint: const Text('انتخاب پورت'),
              items: _availablePorts.map((String value) {
                return DropdownMenuItem<String>(
                  value: value,
                  child: Text(value, overflow: TextOverflow.ellipsis),
                );
              }).toList(),
              onChanged: _isConnected ? null : (newValue) {
                setState(() {
                  _selectedPort = newValue;
                });
              },
            ),
          ),
          if (isWide) const SizedBox(width: 20),
          Row(
            mainAxisAlignment: isWide ? MainAxisAlignment.end : MainAxisAlignment.spaceAround,
            children: [
              ElevatedButton.icon(
                onPressed: _isConnected ? _disconnect : _connect,
                icon: Icon(_isConnected ? Icons.link_off : Icons.link),
                label: Text(_isConnected ? 'قطع اتصال' : 'اتصال'),
                style: ElevatedButton.styleFrom(
                  backgroundColor: _isConnected ? Colors.redAccent : Colors.teal,
                  foregroundColor: Colors.white,
                  padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
                ),
              ),
              IconButton(
                icon: const Icon(Icons.refresh),
                onPressed: _isConnected ? null : _getAvailablePorts,
                tooltip: 'بارگذاری مجدد لیست پورت‌ها',
              ),
            ],
          )
        ];

        return Card(
          child: Padding(
            padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
            child: isWide
                ? Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: children)
                : Column(children: children),
          ),
        );
      },
    );
  }
}

// --- نقاش سفارشی برای صفحه رادار ---

class RadarPainter extends CustomPainter {
  final List<RadarPoint> points;
  final double currentAngle;
  final int maxDistance;

  RadarPainter({required this.points, required this.currentAngle, required this.maxDistance});
  
  @override
  void paint(Canvas canvas, Size size) {
    final center = Offset(size.width / 2, size.height);
    final radius = min(size.width / 2, size.height) * 0.95; // ایجاد یک حاشیه کوچک

    // --- پس‌زمینه ---
    final backgroundPaint = Paint()..color = Colors.black;
    canvas.drawRect(Rect.fromLTWH(0, 0, size.width, size.height), backgroundPaint);
    
    final backgroundGridPaint = Paint()
        ..color = Colors.green.withOpacity(0.05)
        ..style = PaintingStyle.stroke;
    for (var i = 1; i <= 20; i++) {
        canvas.drawLine(Offset(i * size.width / 20, 0), Offset(i * size.width / 20, size.height), backgroundGridPaint);
        canvas.drawLine(Offset(0, i * size.height / 20), Offset(size.width, i * size.height / 20), backgroundGridPaint);
    }

    // --- خطوط شبکه و برچسب‌ها ---
    final gridPaint = Paint()
      ..color = Colors.green.withOpacity(0.5)
      ..style = PaintingStyle.stroke
      ..strokeWidth = 1.0;

    final thinGridPaint = Paint()
      ..color = Colors.green.withOpacity(0.3)
      ..style = PaintingStyle.stroke
      ..strokeWidth = 0.5;

    for (int i = 1; i <= 4; i++) {
        canvas.drawArc(
            Rect.fromCircle(center: center, radius: radius * i / 4),
            pi, pi, false, gridPaint);
    }

    final double fontSize = max(8, radius * 0.04);
    final textPainter = TextPainter(
      textAlign: TextAlign.center,
      textDirection: TextDirection.ltr,
    );
    final textStyle = TextStyle(color: Colors.green.withOpacity(0.8), fontSize: fontSize);

    for (int i = 1; i <= 4; i++) {
        final distance = (maxDistance / 4 * i).toInt();
        final textSpan = TextSpan(text: '${distance}cm', style: textStyle);
        textPainter.text = textSpan;
        textPainter.layout();
        textPainter.paint(canvas, Offset(center.dx - textPainter.width / 2, center.dy - (radius * i / 4) - (fontSize * 1.5)));
    }
    
    for (int i = 0; i <= 180; i += 15) {
        final angleRad = i * pi / 180.0;
        final isMajorLine = i % 45 == 0;
        
        final endPoint = Offset(
            center.dx - radius * cos(angleRad),
            center.dy - radius * sin(angleRad)
        );
        canvas.drawLine(center, endPoint, isMajorLine ? gridPaint : thinGridPaint);

        if (isMajorLine) {
            final labelRadius = radius + (fontSize * 1.5);
            final textSpan = TextSpan(text: '$i°', style: textStyle);
            textPainter.text = textSpan;
            textPainter.layout();
            final labelOffset = Offset(
                center.dx - labelRadius * cos(angleRad) - textPainter.width/2,
                center.dy - labelRadius * sin(angleRad) - textPainter.height/2
            );
            textPainter.paint(canvas, labelOffset);
        }
    }
    
    // --- نقاط و آرک‌های شناسایی شده با افکت محو شدن ---
    final int now = DateTime.now().millisecondsSinceEpoch;
    const double fadeDuration = 4000; // 4 ثانیه برای محو شدن کامل
    final double sensorAngleRad = 30 * (pi / 180.0);

    for (final point in points) {
        final double age = (now - point.timestamp).toDouble();
        
        if (age < fadeDuration) {
            // شفافیت از 1.0 به 0.0 در طول زمان محو شدن کاهش می‌یابد
            final double opacity = 1.0 - (age / fadeDuration);
            final angleRad = point.angle * (pi / 180.0);
            final distance = (point.distance / maxDistance) * radius;

            if (distance < radius) {
                // رنگ‌ها با شفافیت محاسبه شده اعمال می‌شوند
                final detectionArcPaint = Paint()
                    ..color = Colors.lightGreenAccent.withOpacity(0.2 * opacity)
                    ..style = PaintingStyle.fill;
                final pointPaint = Paint()..color = Colors.lightGreenAccent.withOpacity(opacity);

                canvas.drawArc(
                  Rect.fromCircle(center: center, radius: distance), 
                  pi - angleRad - (sensorAngleRad / 2), 
                  sensorAngleRad, 
                  true, 
                  detectionArcPaint
                );

                final pointOffset = Offset(
                    center.dx - distance * cos(angleRad),
                    center.dy - distance * sin(angleRad)
                );
                canvas.drawCircle(pointOffset, 2.0, pointPaint);
            }
        }
    }

    // --- خط رادار (روی همه چیز کشیده می‌شود) ---
    final sweepAngleRad = (currentAngle) * (pi / 180.0);
    final sweepPaint = Paint()
      ..color = Colors.green
      ..strokeWidth = 2.0;
    
    // --- مخروط دنباله خط رادار (اصلاح شده) ---
    final coneAngle = 45 * (pi / 180);
    final gradient = SweepGradient(
      center: const Alignment(0.0, 1.0),
      colors: [Colors.green.withOpacity(0.5), Colors.green.withOpacity(0.0)], // از پررنگ به شفاف
      stops: const [0.0, 1.0],
      startAngle: (180 * pi / 180) - sweepAngleRad - coneAngle, // شروع از پشت خط
      endAngle: (180 * pi / 180) - sweepAngleRad,               // پایان در محل خط
      transform: const GradientRotation(-pi),
    );

    final gradientPaint = Paint()..shader = gradient.createShader(Rect.fromCircle(center: center, radius: radius));
    canvas.drawArc(Rect.fromCircle(center: center, radius: radius), (pi) - sweepAngleRad - coneAngle, coneAngle, true, gradientPaint);

    // خط اصلی رادار در بالای گرادیان کشیده می‌شود
    final sweepEnd = Offset(
        center.dx - radius * cos(sweepAngleRad),
        center.dy - radius * sin(sweepAngleRad)
    );
    canvas.drawLine(center, sweepEnd, sweepPaint);
  }

  @override
  bool shouldRepaint(covariant CustomPainter oldDelegate) {
    return true; // همیشه دوباره نقاشی کن
  }
}


				
			

جمع‌بندی: پروژه رادار با آردوینو و فلاتر

    1. آردوینو سروو را یک درجه حرکت می‌دهد.

    2. سنسور فاصله را اندازه‌گیری می‌کند.

    3. آردوینو یک رشته متنی مانند "45,78" (به معنی زاویه 45 درجه، فاصله 78 سانتی‌متر) را از طریق USB ارسال می‌کند.

    4. اپلیکیشن فلاتر که به پورت سریال گوش می‌دهد، این رشته را دریافت و解析 می‌کند.

    5. با استفاده از CustomPainter در فلاتر، برنامه خط اسکنر را در زاویه جدید (45 درجه) رسم کرده و یک شیء را در فاصله مشخص شده (78 سانتی‌متر) با افکت‌های بصری نمایش می‌دهد.

    6. این فرآیند برای تمام زوایا از 0 تا 180 و برعکس به سرعت تکرار می‌شود و در نتیجه یک تصویر زنده و پویا از محیط اطراف ایجاد می‌گردد.

این پروژه یک نمونه عالی از قدرت ترکیب سخت‌افزارهای ساده و قابل دسترس مانند آردوینو با فریمورک‌های نرم‌افزاری قدرتمند مانند فلاتر برای خلق محصولات کاربردی و جذاب است.

طراحی و توسعه توسط: شرکت مخترعین شاتوت الکترونیک | shahtut.com

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

برای امنیت، استفاده از سرویس کپتچا گوگل مورد نیاز است که تابع گوگل است سیاست حفظ حریم خصوصی و شرایط استفاده.

Iبا این شرایط موافقید.