Need help using 7 color e-paper display

I bought a 7.3in 7 color display, and I was able to make it work with the waveshare demo code and with gxepd2 examples from @ZinggJM. But the reason why I bought this display was to make a digital portrait frame, so I found a nice repository from github user dani3lwinter that had exaclty what I needed. After hours adapting the code, I hit a dead end. The code works flawlessly with small images, but when I upload full resolution images (800x480) the colors are not displayed correctly and with some noise. I made a fork with my code and the issues is either on the html functions that pre-process the image or loadImage function that receives the data on display.h. can someone help me find the issue?

Check the "examples" folder from the library. You should post your example code.

The code used on display.h file is based on the instructions defined in the .cpp and .h files from waveshare for this display

All files are here

After the post, I began testing some different resolutions. Here's what I found: below 200x200 colors are fine, between that and 400x400 image is black and white, and above 400x400 it appears yellowish as pictures above. I don't know why and I will check on this later, my head is boiling. Hopefully someone will figure this out

I think the problem is dithering, it's done by the code that's in the html file.
I tried to follow him but it is somewhat confusing.
It is to review it with great tranquility.

No, that is a link to "i don't care (any more)."

1 Like

Thanks, @MaximoEsfuerzo I'm also starting to think the issue may be on the html because of the test I mentioned above. The original code that I forked used a 3 color screen (black, white, red) with 640x384 resolution, so maybe this is why some images appear only black/white on some resolutions and with incorrect colors above that. I still need to figure out why 200x200 images and below worked tho

@xfpd main code

#include "WiFi.h"
#include "SPIFFS.h"
#include "ESPAsyncWebServer.h"
#include "credentials.h"
#include "display.h"

AsyncWebServer server(80);
AsyncWebSocket ws("/test");

TaskHandle_t Task1 = NULL;

void driveDisplay(void *parameter) {  //running on another core to avoid watchdog timer error
  while (true) {
    TurnOnDisplay();
    vTaskSuspend(Task1);
  }
}

void onWsEvent(AsyncWebSocket *server, AsyncWebSocketClient *client,
               AwsEventType type, void *arg, uint8_t *data, size_t len) {

  if (type == WS_EVT_CONNECT) {
    Serial.println("Websocket client connection received");
  } else if (type == WS_EVT_DISCONNECT) {
    Serial.println("Websocket client disconnected");
  } else if (type == WS_EVT_DATA) {
    AwsFrameInfo *info = (AwsFrameInfo *)arg;
    if (info->opcode == WS_TEXT) {  // Got text data (the client checks if the display is busy)
      client->text(responseToNewImage());
    } else {  // Got binary data (the client sent the image)
      // Send the received data to the display buffer.
      loadImage((const char *)data, len);
      if ((info->index + len) == info->len) {  // if this data was the end of the file
        //client->text("IMAGE_LOADED");       // tell the client that the server got the image
        Serial.printf("Updating Display: %d bytes total\n", info->len);
        updateDisplay_withoutIdle();  // Display the image that received
      }
    }
  }
}

char *responseToNewImage() {
  if (isDisplayBusy()) {
    return "BUSY";
  } else {
    sendCommand(0x10);
    return "OK";
  }
}

void Clear(unsigned char color) {
  sendCommand(0x10);
  for (int i = 0; i < 800 / 2; i++) {
    for (int j = 0; j < 480; j++) {
      sendData((color << 4) | color);
    }
  }
  vTaskResume(Task1);  //call TurnOnDisplay()
}

// Shows the loaded image
void updateDisplay_withoutIdle() {
  // Refresh.
  vTaskResume(Task1);  //call TurnOnDisplay()
  Serial.println("Update successful");
}

void setup() {
  Serial.begin(115200);

  if (!SPIFFS.begin()) {
    Serial.println("An Error has occurred while mounting SPIFFS");
    return;
  }
  if (initDisplay() != 0) {
    Serial.println("Init failed");
    return;
  }

  Serial.println("Init successful");

  xTaskCreatePinnedToCore(
    driveDisplay,   /* Task function. */
    "DisplayDrive", /* name of task. */
    32000,          /* Stack size of task */
    NULL,           /* parameter of the task */
    1,              /* priority of the task */
    &Task1,         /* Task handle to keep track of created task */
    0);             /* Core */

  vTaskSuspend(Task1);

  Clear(EPD_7IN3F_WHITE);

  WiFi.mode(WIFI_STA);
  WiFi.begin(wifi_credentials.ssid, wifi_credentials.password);
  Serial.print("Connecting to WiFi ..");
  while (WiFi.status() != WL_CONNECTED) {
    Serial.print('.');
    delay(1000);
  }
  Serial.println(WiFi.localIP());

  delay(2000);

  // setup handles for websockets connetions
  ws.onEvent(onWsEvent);
  server.addHandler(&ws);

  // handle GET requests to route /index.html
  server.on("/index.html", HTTP_GET, [](AsyncWebServerRequest *request) {
    Serial.println("Request recived: GET /index.html");
    request->send(SPIFFS, "/index.html", "text/html");
  });

  // handle GET requests to route /
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request) {
    Serial.println("Request recived: GET /");
    request->send(SPIFFS, "/index.html", "text/html");
  });

  // handle GET requests to route /hello - echo "Hello Word" (for debugging)
  server.on("/hello", HTTP_GET, [](AsyncWebServerRequest *request) {
    Serial.println("Request recived: GET /hello");
    request->send(200, "text/plain", "Hello World");
  });

  server.begin();  //start listening to incoming HTTP requests
}

void loop() {
  //delay(100);
}

display.h header

#ifndef display_h
#define display_h

#include <Arduino.h>
#include <SPI.h>
SPIClass hspi(HSPI);

//some functions were replaced/copied from epd7in3f.cpp from waveshare's demo code

#define EPD_7IN3F_BLACK 0x0   /// 000
#define EPD_7IN3F_WHITE 0x1   ///	001
#define EPD_7IN3F_GREEN 0x2   ///	010
#define EPD_7IN3F_BLUE 0x3    ///	011
#define EPD_7IN3F_RED 0x4     ///	100
#define EPD_7IN3F_YELLOW 0x5  ///	101
#define EPD_7IN3F_ORANGE 0x6  ///	110
#define EPD_7IN3F_CLEAN 0x7   ///	111   unavailable  Afterimage

// SPI pins. Adapt to your wiring
#define PIN_SPI_SCK 12
#define PIN_SPI_DIN 11
#define PIN_SPI_CS 15
#define PIN_SPI_BUSY 14
#define PIN_SPI_RST 17
#define PIN_SPI_DC 16

// Wakes up the display from sleep.
void resetDisplay() {
  digitalWrite(PIN_SPI_RST, HIGH);
  delay(20);
  digitalWrite(PIN_SPI_RST, LOW);  //module reset
  delay(1);
  digitalWrite(PIN_SPI_RST, HIGH);
  delay(20);
}

// Sends one byte via SPI.
void sendSpi(byte data) {
  digitalWrite(PIN_SPI_CS, LOW);
  hspi.transfer(data);
  digitalWrite(PIN_SPI_CS, HIGH);
}

// Sends one byte as a command.
void sendCommand(byte command) {
  digitalWrite(PIN_SPI_DC, LOW);
  hspi.transfer(command);
}

// Sends one byte as data.
void sendData(byte data) {
  digitalWrite(PIN_SPI_DC, HIGH);
  hspi.transfer(data);
}

// Waits until the display is ready.
void waitForIdle() {
  while (digitalRead(PIN_SPI_BUSY) == LOW /* busy */) {
    Serial.print(".");
    delay(100);
  }
}

// Returns whether the display is busy
bool isDisplayBusy() {
  return (digitalRead(PIN_SPI_BUSY) == LOW);
}

void EPD_7IN3F_BusyHigh()  // If BUSYN=0 then waiting
{
  while (!digitalRead(PIN_SPI_BUSY)) {
    delay(1);
  }
}

void TurnOnDisplay() {  //runs on another core to avoid watchdog timer error
  Serial.println("power on");
  sendCommand(0x04);  // POWER_ON
  EPD_7IN3F_BusyHigh();

  Serial.println("refresh");
  sendCommand(0x12);  // DISPLAY_REFRESH
  sendData(0x00);
  EPD_7IN3F_BusyHigh();

  Serial.println("power off");
  sendCommand(0x02);  // POWER_OFF
  sendData(0x00);
  EPD_7IN3F_BusyHigh();
}

int IfInit(void) {
  // Initialize SPI.
  pinMode(PIN_SPI_CS, OUTPUT);
  pinMode(PIN_SPI_RST, OUTPUT);
  pinMode(PIN_SPI_DC, OUTPUT);
  pinMode(PIN_SPI_BUSY, INPUT);
  hspi.begin(PIN_SPI_SCK, -1, PIN_SPI_DIN);
  hspi.beginTransaction(SPISettings(4000000, MSBFIRST, SPI_MODE0));

  return 0;
}

// Initializes the display.
int initDisplay(void) {
  Serial.println("Initializing display");
  if (IfInit() != 0) {
    return -1;
  }

  // Initialize the display.
  resetDisplay();
  delay(20);
  EPD_7IN3F_BusyHigh();

  sendCommand(0xAA);  // CMDH
  sendData(0x49);
  sendData(0x55);
  sendData(0x20);
  sendData(0x08);
  sendData(0x09);
  sendData(0x18);

  sendCommand(0x01);
  sendData(0x3F);
  sendData(0x00);
  sendData(0x32);
  sendData(0x2A);
  sendData(0x0E);
  sendData(0x2A);

  sendCommand(0x00);
  sendData(0x5F);
  sendData(0x69);

  sendCommand(0x03);
  sendData(0x00);
  sendData(0x54);
  sendData(0x00);
  sendData(0x44);

  sendCommand(0x05);
  sendData(0x40);
  sendData(0x1F);
  sendData(0x1F);
  sendData(0x2C);

  sendCommand(0x06);
  sendData(0x6F);
  sendData(0x1F);
  sendData(0x1F);
  sendData(0x22);

  sendCommand(0x08);
  sendData(0x6F);
  sendData(0x1F);
  sendData(0x1F);
  sendData(0x22);

  sendCommand(0x13);  // IPC
  sendData(0x00);
  sendData(0x04);

  sendCommand(0x30);
  sendData(0x3C);

  sendCommand(0x41);  // TSE
  sendData(0x00);

  sendCommand(0x50);
  sendData(0x3F);

  sendCommand(0x60);
  sendData(0x02);
  sendData(0x00);

  sendCommand(0x61);
  sendData(0x03);
  sendData(0x20);
  sendData(0x01);
  sendData(0xE0);

  sendCommand(0x82);
  sendData(0x1E);

  sendCommand(0x84);
  sendData(0x00);

  sendCommand(0x86);  // AGID
  sendData(0x00);

  sendCommand(0xE3);
  sendData(0x2F);

  sendCommand(0xE0);  // CCSET
  sendData(0x00);

  sendCommand(0xE6);  // TSSET
  sendData(0x00);

  return 0;
}

void Sleep(void) {
  sendCommand(0x07);
  sendData(0xA5);
  delay(10);
  digitalWrite(PIN_SPI_RST, 0);  // Reset
}

// Converts one pixel from input encoding (2 bits) to output encoding (4 bits).
byte convertPixel(byte value) {
  switch (value) {
    case 0x0:
      return EPD_7IN3F_BLACK;  // BLACK
    case 0x1:
      return EPD_7IN3F_WHITE;  // WHITE
    case 0x2:
      return EPD_7IN3F_GREEN;  // GREEN
    case 0x3:
      return EPD_7IN3F_BLUE;  // BLUE
    case 0x4:
      return EPD_7IN3F_RED;  // RED
    case 0x5:
      return EPD_7IN3F_YELLOW;  // YELLOW
    case 0x6:
      return EPD_7IN3F_ORANGE;  // ORANGE
    default:
      return EPD_7IN3F_CLEAN;  // CLEAN
  }
}

// Loads partial image data onto the display.
// Loads image data onto the display.
void loadImage(const char* image_data, size_t length) {
    Serial.printf("Loading image data: %d bytes\n", length);

    for (size_t i = 0; i < length; i++) {
        // Extract 4-bit color values for two pixels from each byte
        byte p1 = (image_data[i] >> 4) & 0x0F; // Upper 4 bits (first pixel)
        byte p2 = image_data[i] & 0x0F;        // Lower 4 bits (second pixel)

        // Combine these into one byte to send as two packed pixels
        sendData((p1 << 4) | p2);
    }
}

#endif  // display_h

this is the webpage stored in SPIFFS that handles image processing and sends to the display via esp32

<!DOCTYPE html>
<html>
<style>
.slidecontainer {
  width: 100%;
}

.slider {
  -webkit-appearance: none;
  width: 100%;
  height: 15px;
  border-radius: 5px;
  background: #d3d3d3;
  outline: none;
  opacity: 0.7;
  -webkit-transition: .2s;
  transition: opacity .2s;
}

.slider:hover {
  opacity: 1;
}

.slider::-webkit-slider-thumb {
  -webkit-appearance: none;
  appearance: none;
  width: 25px;
  height: 25px;
  border-radius: 50%;
  background: #800000;
  cursor: pointer;
}

.slider::-moz-range-thumb {
  width: 25px;
  height: 25px;
  border-radius: 50%;
  background: #800000;
  cursor: pointer;
}

input[type="file"] {
    display: none;
}

.red-button {
  width: 140px;
  background-color: #cc0000;
  color: white;
  text-align: center;
  display: inline-block;
  font-size: 16px;
  padding: 14px 0px;
  margin: 0px 4px 16px;
  border-radius: 8px;
  box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2), 0 6px 20px 0 rgba(0,0,0,0.19);
  cursor: pointer;
  border: 2px solid #800000;
}
.red-button:hover {
	box-shadow: none;
}

th, td {
  padding: 10px 4px;
  text-align:left;
}

body{
  background:#f2f2f2;
  font-family:Arial, Helvetica, sans-serif;
}

</style>	
<head>
</head>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<body>
	<div style="max-width: 100%; margin: 8px; text-align:center;">
		<h1>Smart E-Paper Frame</h1>
		
		<label class="red-button">
			<input type="file"  id="inputFile"/>Load Image
		</label>
		<button class="red-button" onclick="dither()">Dither</button>
		<button class="red-button" onclick="stretchToFit()">Stretch to Fit</button>
		</br>
		
		<div  style="width: 100%; display: none;" >
			<canvas style="width: 100%;" width="800" height="480" id="originalCanvas"/>
		</div>
		<div style="width: 100%; border: 3px solid black; border-radius: 5px; background:white;" >
			<canvas style="width: 100%;" width="800" height="480"  id="editedCanvas"/>
		</div>
		
		<button onclick="uploadImage()" class="red-button" style="width: 100%; margin: 16px 2px 5px;">Upload to Frame</button>
	</div>

	<script>
	
	// Map colors to their 4-bit indices
	const COLOR_MAP = {
		"0,0,0": 0,        // Black
		"255,255,255": 1,  // White
		"0,255,0": 2,      // Green
		"0,0,255": 3,      // Blue
		"255,0,0": 4,      // Red
		"255,255,0": 5,    // Yellow
		"255,165,0": 6     // Orange
	};

	// Update rbg2bit to map RGB values to indices
	function rbg2bit(r, g, b) {
		const key = `${r},${g},${b}`;
		return COLOR_MAP[key] !== undefined ? COLOR_MAP[key] : 0; // Default to black if undefined
	}
	
	var PALETTE = [[0  , 0  , 0  ], [255, 255, 255], [128, 0  , 0  ]];
	function uploadImage(){
		console.log("uploadImage");
		
		/*bytesArray = new Uint8Array(pixDataArray);
		var xhr = new XMLHttpRequest();
		xhr.open('POST', '/post',true);
		xhr.setRequestHeader('Content-Type', 'application/octet-stream');
		xhr.send(bytesArray);*/
		
		websocket = new WebSocket("ws://" + location.hostname +"/test");
		websocket.onopen = function(evt) { onOpen(evt) };
		websocket.onclose = function(evt) { onClose(evt) };
		websocket.onmessage = function(evt) { onMessage(evt) };
		websocket.onerror = function(evt) { onError(evt) };
	}
	
	
	function initWebSocket(){
		
	}

	function onOpen(evt){
		console.log("CONNECTED");
		websocket.send("Can I send data?");
	}

	function onClose(evt){
		console.log("DISCONNECTED");
	}

	function onMessage(evt){
		console.log("RESPONSE:");	
		console.log(evt.data);
		
		if(evt.data == "OK"){
			sendImage();
		}
		else if(evt.data == "BUSY"){
			alert("The frame is busy - Can't load new image");
			websocket.close();
		}
		else{
			websocket.close();
		}	
	}
	
	function sendImage() {
		const ctx = document.getElementById('editedCanvas').getContext('2d');
		const imgData = ctx.getImageData(0, 0, 800, 480);
		const d = imgData.data;
		const pixDataArray = new Array((800 * 480) / 2); // 1 byte per 2 pixels
		let j = 0;

		for (let i = 0; i < d.length; i += 8) { // Process two pixels at a time (8 bytes per two pixels)
			const pix0 = rbg2bit(d[i], d[i + 1], d[i + 2]);       // Pixel 1
			const pix1 = rbg2bit(d[i + 4], d[i + 5], d[i + 6]);   // Pixel 2
			const pixByte = (pix0 << 4) | pix1; // Combine into a single byte
			pixDataArray[j++] = pixByte;
		}

		const bytesArray = new Uint8Array(pixDataArray);
		websocket.send(bytesArray.buffer); // Send the packed data
		console.log("SENT");
		websocket.close();
	}
	
	function onError(evt){
		console.log("ERROR:");
		console.log(evt.data);//undifened
	}

	var ditherLevel = 16;
	var sliderDither = 16;
	var outputDither = 16;
	outputDither.innerHTML = sliderDither.value;
	sliderDither.oninput = function() {
	  outputDither.innerHTML = this.value;
	  ditherLevel = this.value;
	};
	
	var sliderContrast = 0;
	var outputContrast = 0;
	outputContrast.innerHTML = sliderContrast.value;
	sliderContrast.oninput = function() {
	
		outputContrast.innerHTML = this.value;
		var ctx = document.getElementById('originalCanvas').getContext('2d');
		var imgData = ctx.getImageData(0,0,800,480);
		
		imgData = contrastImage(imgData, this.value);
		
		ctx = document.getElementById('editedCanvas').getContext('2d');
		ctx.putImageData(imgData, 0, 0);
	};
	
	function contrastImage(imgData, contrast){  /*input range [-100..100]*/
		var d = imgData.data;
		contrast = (contrast/100) + 1;  /*convert to decimal & shift range: [0..2]*/
		var intercept = 128 * (1 - contrast);
		for(var i=0;i<d.length;i+=4){   /*r,g,b,a*/
			d[i] = d[i]*contrast + intercept;
			d[i+1] = d[i+1]*contrast + intercept;
			d[i+2] = d[i+2]*contrast + intercept;
		}
		return imgData;
	}
	
	var input = document.getElementById('inputFile');
	input.addEventListener('change', handleFiles);

	/*load the file to the two canvases*/
	function handleFiles(e) {
		var ctxOrigin = document.getElementById('originalCanvas').getContext('2d');
		var ctxEdited = document.getElementById('editedCanvas').getContext('2d');
		var img = new Image();
		img.src = URL.createObjectURL(e.target.files[0]);
		img.onload = function () {
			const canvasWidth = 800;
			const canvasHeight = 480;

			// Clear the canvas and fill it with white
			ctxOrigin.fillStyle = "white";
			ctxOrigin.fillRect(0, 0, canvasWidth, canvasHeight);
			ctxEdited.fillStyle = "white";
			ctxEdited.fillRect(0, 0, canvasWidth, canvasHeight);

			// Calculate centered position without scaling
			const offsetX = (canvasWidth - img.width) / 2;
			const offsetY = (canvasHeight - img.height) / 2;

			// Draw the image centered without stretching
			ctxOrigin.drawImage(img, 0, 0, img.width, img.height, offsetX, offsetY, img.width, img.height);
			ctxEdited.drawImage(img, 0, 0, img.width, img.height, offsetX, offsetY, img.width, img.height);
		};
	}



	    var PALETTE = [
        [0, 0, 0],        // Black
        [255, 255, 255],  // White
        [0, 255, 0],      // Green
        [0, 0, 255],      // Blue
        [255, 0, 0],      // Red
        [255, 255, 0],    // Yellow
        [255, 165, 0]     // Orange
    ];
					 
	function dist_to_pixel(r, g, b, color){
		var d1 = Math.abs(r-color[0]);
		var d2 = Math.abs(g-color[1]);
		var d3 = Math.abs(b-color[2]);
		return (d1 + d2 + d3);
	}
	
    function find_closest_palette_color(r, g, b) {
        let min_dist = Infinity;
        let closest_color = [0, 0, 0];
        for (let color of PALETTE) {
            let dist = Math.sqrt(
                Math.pow(r - color[0], 2) +
                Math.pow(g - color[1], 2) +
                Math.pow(b - color[2], 2)
            );
            if (dist < min_dist) {
                min_dist = dist;
                closest_color = color;
            }
        }
        return closest_color;
    }
	
    function addToPixel(pixels, pos, quant_error, fraction) {
        if (pos >= 0 && pos < pixels.length) {
            for (let i = 0; i < 3; i++) {
                pixels[pos + i] += quant_error[i] * fraction;
                pixels[pos + i] = Math.min(255, Math.max(0, pixels[pos + i])); // Clamp values
            }
        }
    }

    function dither() {
        const R = 0, G = 1, B = 2, A = 3;
        const width = 800, height = 480;

        const ctx = document.getElementById('editedCanvas').getContext('2d');
        const imgData = ctx.getImageData(0, 0, width, height);
        const pixels = imgData.data;

        for (let y = 0; y < height; y++) {
            for (let x = 0; x < width; x++) {
                const pos = (y * width + x) * 4;
                const oldR = pixels[pos + R];
                const oldG = pixels[pos + G];
                const oldB = pixels[pos + B];
                
                const newRGB = find_closest_palette_color(oldR, oldG, oldB);
                pixels[pos + R] = newRGB[0];
                pixels[pos + G] = newRGB[1];
                pixels[pos + B] = newRGB[2];
                pixels[pos + A] = 255; // Fully opaque

                const quant_error = [
                    oldR - newRGB[0],
                    oldG - newRGB[1],
                    oldB - newRGB[2]
                ];

                // Error diffusion
                addToPixel(pixels, pos + 4, quant_error, 7.0 / ditherLevel); // Right
                addToPixel(pixels, pos - 4 + (width * 4), quant_error, 3.0 / ditherLevel); // Bottom-left
                addToPixel(pixels, pos + (width * 4), quant_error, 5.0 / ditherLevel); // Bottom
                addToPixel(pixels, pos + 4 + (width * 4), quant_error, 1.0 / ditherLevel); // Bottom-right
            }
        }

        ctx.putImageData(imgData, 0, 0);
    }
	
	function stretchToFit() {
		const ctxEdited = document.getElementById('editedCanvas').getContext('2d');
		const img = new Image();
		img.src = document.getElementById('inputFile').files[0]
			? URL.createObjectURL(document.getElementById('inputFile').files[0])
			: null;

		if (!img.src) {
			alert("Please upload an image first!");
			return;
		}

		img.onload = function () {
			const canvasWidth = 800;
			const canvasHeight = 480;

			// Clear the canvas and fill it with white
			ctxEdited.fillStyle = "white";
			ctxEdited.fillRect(0, 0, canvasWidth, canvasHeight);

			// Calculate scaling factors for width and height
			const scaleWidth = canvasWidth / img.width;
			const scaleHeight = canvasHeight / img.height;

			// Use the smaller scale factor to maintain aspect ratio
			const scale = Math.min(scaleWidth, scaleHeight);

			// Calculate the new dimensions of the image
			const newWidth = img.width * scale;
			const newHeight = img.height * scale;

			// Center the scaled image on the canvas
			const offsetX = (canvasWidth - newWidth) / 2;
			const offsetY = (canvasHeight - newHeight) / 2;

			// Draw the image with maintained proportions
			ctxEdited.drawImage(img, 0, 0, img.width, img.height, offsetX, offsetY, newWidth, newHeight);
		};
	}
	
	</script>
</body>
</html>

I saw this topic where other people mention the same issue with this display and there are 2 solutions: hardware solution using DESPI-C73 instead of waveshare's paper HAT and a software solution "do a semi reset and try again" according to user @waldow1 but how to do it remains unknown

Hi @daneins . Nice project .Do you still need help? My solution was not 100% . But I can help .

Yes, if you could help explaining how to do a soft reset, I would appreciate

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.